zk-nullifier
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseZK 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
工作流程
- Clarify intent
- Recommend plan mode, if it's not activated
- Use to resolve blind spots
AskUserQuestion - All questions must be resolved before execution
- Identify references and skills
- Match task to resources below
- Locate relevant documentation and examples
- Write plan file (YAML task format)
- Use for anything unclear — never guess or assume
AskUserQuestion - Identify blockers: permissions, dependencies, unknowns
- Plan must be complete before execution begins
- Use
- Execute
- Use tool with subagents for parallel research
Task - Subagents load skills via tool
Skill - Track progress with
TodoWrite
- Use
- When stuck: spawn subagent with ,
Read,Glob, DeepWiki MCP access and loadGrepskills/ask-mcp
- 明确意图
- 如果计划模式未激活,建议开启
- 使用解决盲区
AskUserQuestion - 执行前必须解决所有问题
- 识别参考资料和技能
- 将任务与下方的资源匹配
- 定位相关文档和示例
- 编写计划文件(YAML任务格式)
- 任何不清楚的地方都使用——绝不猜测或假设
AskUserQuestion - 识别障碍:权限、依赖项、未知因素
- 计划必须完整才能开始执行
- 任何不清楚的地方都使用
- 执行
- 使用工具和子代理进行并行研究
Task - 子代理通过工具加载技能
Skill - 使用跟踪进度
TodoWrite
- 使用
- 遇到问题时:生成带有、
Read、Glob、DeepWiki MCP访问权限的子代理,并加载Grepskills/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).
| Storage | Cost per nullifier |
|---|---|
| PDA | 890,880 lamports |
| Compressed PDA | 15,000 lamports |
Nullifier是一种确定性派生的哈希,用于确保某个操作只能执行一次。Nullifier无法与操作或用户关联。例如Zcash使用nullifier来防止双重支付。
要实现nullifier,我们需要一种数据结构来确保每个nullifier只创建一次且永不删除。在Solana上,实现nullifier的一种直接方式是创建以nullifier为种子的PDA账户。
PDA账户无法关闭,并且每个nullifier需要永久锁定890,880个lamports(达到免租金要求)。
压缩PDA的派生方式与Solana PDA类似,创建成本为15,000个lamports(无免租金要求)。
| 存储方式 | 每个nullifier的成本 |
|---|---|
| PDA | 890,880 lamports |
| 压缩PDA | 15,000 lamports |
Testing
测试
bash
undefinedbash
undefinedRust 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
undefinedlight test-validator # 单独终端
npm run test:ts
undefinedPattern 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 exists1. 客户端计算nullifier = hash(secret, context)
2. 客户端获取派生地址的有效性证明(证明该地址不存在)
3. 客户端调用create_nullifier并传入nullifier值和证明
4. 程序从nullifier派生地址,通过CPI创建压缩账户
5. 如果地址已存在,轻量系统程序会拒绝CPI调用Resources
资源
- Full example: program-examples/zk/nullifier
- ZK overview: zkcompression.com/zk/overview
- Additional ZK examples: program-examples/zk (nullifier, zk-id, mixer, shielded-pool)
- 完整示例:program-examples/zk/nullifier
- ZK概述:zkcompression.com/zk/overview
- 更多ZK示例:program-examples/zk(nullifier、zk-id、mixer、shielded-pool)
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)?;