11. 白名单与权限控制

白名单与权限控制

在热门 NFT 发售中,白名单 (Whitelist) 是至关重要的机制。它用于奖励早期社区成员、防止科学家(Bot)抢跑,以及避免 Gas War(网络拥堵)。

实现白名单有多种技术方案,核心挑战在于:如何在不消耗巨额链上存储成本的前提下,验证数千个地址的资格?


1. 方案对比

方案原理优点缺点
链上存储 (Map)将所有白名单地址写入一个巨大的 PDA 账户。逻辑最简单,实时性高。成本极高。存 1000 个地址需要大量 SOL 租金,且受账户大小限制。
Merkle Tree将地址列表构建成哈希树,链上只存 根哈希 (Root Hash)成本极低 (仅存 32 字节),支持无限数量地址。需要前端配合生成证明 (Proof),逻辑稍复杂。
签名验证 (Sig)后端服务器对用户地址签名,链上验证签名。极度灵活(可动态调整),零存储成本。中心化依赖。如果服务器私钥泄露或宕机,铸造将停止。

在 Solana 生态中,Merkle Tree 是最主流的去中心化方案。


2. Merkle Tree 白名单实现

Merkle Tree 的魔法在于:你不需要知道整棵树的所有叶子,只需要知道通往根节点的路径上的“邻居”哈希值,就能证明你属于这棵树。

2.1 链下:生成树与证明 (TypeScript)

在发售前,项目方在本地计算出 Merkle Root,并将其写入铸造程序的配置账户中。

typescript
import { MerkleTree } from "merkletreejs"; import keccak256 from "keccak256"; import { PublicKey } from "@solana/web3.js"; // 1. 准备白名单 const whitelistAddresses = [ "User1...", "User2...", "User3..." ]; // 2. 将地址哈希化 (作为叶子节点) const leafNodes = whitelistAddresses.map(addr => keccak256(new PublicKey(addr).toBuffer()) ); // 3. 构建树 const tree = new MerkleTree(leafNodes, keccak256, { sortPairs: true }); // 4. 获取根哈希 (存入链上) const root = tree.getRoot(); console.log("Merkle Root:", root.toString('hex')); // 5. 为前端用户生成证明 // 当 User1 来铸造时,前端需要计算这个 Proof 传给合约 const leaf = keccak256(new PublicKey("User1...").toBuffer()); const proof = tree.getProof(leaf).map(p => p.data);

2.2 链上:验证证明 (Anchor Rust)

在铸造指令中,合约接收用户提供的 proof,尝试结合用户的地址(leaf)重新计算根哈希。如果计算结果与存储的 merkle_root 一致,说明用户在白名单内。

rust
use anchor_lang::solana_program::keccak; pub fn whitelist_mint( ctx: Context<WhitelistMint>, proof: Vec<[u8; 32]>, // 用户传入的证明路径 ) -> Result<()> { let config = &ctx.accounts.config; let minter = ctx.accounts.minter.key(); // 1. 计算当前用户的叶子节点哈希 let leaf = keccak::hashv(&[minter.as_ref()]).0; // 2. 验证 Merkle Proof require!( verify_merkle_proof(config.merkle_root, leaf, proof), MinterError::NotWhitelisted ); // ... 执行铸造逻辑 Ok(()) } // 验证逻辑:沿着路径向上哈希,直到根节点 fn verify_merkle_proof( root: [u8; 32], leaf: [u8; 32], proof: Vec<[u8; 32]>, ) -> bool { let mut computed_hash = leaf; for proof_element in proof.iter() { if computed_hash <= *proof_element { computed_hash = keccak::hashv(&[&computed_hash, proof_element]).0; } else { computed_hash = keccak::hashv(&[proof_element, &computed_hash]).0; } } computed_hash == root }

3. 签名验证方案 (Signature Verification)

这种方案适用于需要动态逻辑的场景(例如:必须关注 Twitter 才能铸造,或者白名单每分钟都在变)。

原理

  1. 后端:配置一个 Admin Keypair
  2. 前端:用户请求后端,后端验证资格后,用 Admin Private Key 对消息 "Allow Mint: UserAddr, ExpiryTime" 进行签名。
  3. 链上:用户发送交易时携带这个签名。合约使用存储的 Admin Public Key 验证签名是否有效且未过期。
rust
// Anchor 指令中验证 Ed25519 签名 use anchor_lang::solana_program::ed25519_program; // 注意:Solana 有专门的 Ed25519 预编译指令检查机制 // 通常的做法是将 Ed25519 验证指令作为交易的第一条指令 (Instruction Introspection)

或者在合约内部简单验证(消耗较多 Compute Units,不推荐用于复杂逻辑): 这种方式通常依赖 load_instruction_at_checked 检查交易中是否包含 Ed25519 程序的验证指令。

TSPlayground
EDITOR ACTIVE
Initializing TS Environment...

Merkle Tree 验证器

On-Chain Root: 0xABCD
ROOT
0xABCD
Hash 1
0xAB
Hash 2
0xCD
Alice
0xA1
Bob
0xB2
Charlie
0xC3
David
0xD4
请选择一个用户进行 Merkle Proof 验证