免费铸造的白名单活动,用户侧的体验通常是一句话:“输入钱包地址,网站告诉你有没有资格。“但链上其实不存在一份十万地址的名单——合约里没有一个函数能遍历名单问”你是谁”。真正发生的是一种反向证明:名单被压缩成一个 32 字节的哈希存进合约,你自己带着证据来,合约当场验算。这套机制叫默克尔申领,前身是空投分发,后来被 NFT 白名单原样借用。
一个根,一堆叶子
机制的地基是一棵默克尔树:把每个领取条目当作一片叶子,两两哈希上卷,最后收敛出一个根哈希。领取条目长什么样,Uniswap 当年的分发合约写得最直白,它的 claim 函数签名是 claim(uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof)——序号、地址、数量三个数拼成叶子,外加一串兄弟节点哈希作为证明。项目方离线用这棵树生成根,把根写进链上合约;树本身、名单本身,链上都没有存。
用户要做的,是向申领前端提供自己的地址,前端(或脚本)在名单里找到自己的行号,取出对应的叶子和一路的兄弟节点,打包成证明提交。合约端的验算只有三步:第一步,isClaimed(index) 查这个序号是否已被领过,领过直接 revert;第二步,把 index、account、amount 用 keccak256(abi.encodePacked(...)) 现场重新拼出叶子;第三步,调用 OpenZeppelin 的 MerkleProof.verify,从叶子出发沿着你交来的兄弟哈希逐级上卷,看得到的根是否等于合约里那个根。三步全过,代币或铸造资格才发放。整套设计里没有任何信任环节需要”相信名单存在”——你只需要拿出能与根对上的证据。

工程细节里埋着的坑
OpenZeppelin 的库文档顺手记录了两个经典陷阱。其一是 64 字节叶子的碰撞问题:默认的 processProof 采用”排序对哈希”——把相邻两个节点排序后拼接再哈希;当叶子恰好是两个 32 字节量拼成 64 字节时,攻击者可以把”地址拼数量”重新解释成”两个哈希的排序对”,伪造出合法的证明路径。所以这份文档给出的警告是针对使用 64 字节叶子的合约:“请考虑使用 keccak256(abi.encodePacked(leaf)) 或类似变换,让叶子不再是裸的 64 字节值”。Uniswap 式合约把 index 也拼进叶子,正是规避手段之一。其二是防重放靠序号:isClaimed 记录的是行号,一个地址在名单里有十行,就有十份独立的领取资格,而同一行永远只能花一次——把白名单当”身份”反复使用是新手最常见的误解。
普通用户能核对什么
这套机制给了收藏者三个具体的自查动作。第一,看根:申领开始前合约是否已部署且 merkleRoot 是真实哈希而非全零占位,根一改等于换名单,读合约即可确认。第二,看事件:claim 成功会发出带行号、地址、数量的 Claimed 事件,你在区块浏览器上能找到自己那一条才算真领到。第三,保存证据:把前端给你的 JSON 证明存档,它是”你当时有资格”的唯一可复核材料——名单页截图什么也不能证明,证明对象是那棵树的根。反过来也成立:项目方想临时塞人进名单,必须重算整棵树并换根上链,改名单不是一次静默的数据库操作。默克尔白名单常被宣传成”去中心化名单”,更准确的说法是:名单仍是项目方的数据,只是每次修改都要过一遍链上哈希的安检门。
时间窗口与未领份额
还有一类细节常被忽略:申领类合约普遍把窗口写进规则,Uniswap 当年的分发就是按时段开放的领取设计,NFT 白名单铸造同样常配截止。合约模式各异,有的截止后未领份额退回项目方预留池,有的直接让 claim 拒绝执行。对参与者,这意味着证明有保质期:今天能对上一棵新根的叶子,过期后照样对得上根,但合约不再理它。领与不领都是决策而不是默认状态——把资格有效期写进日历,比事后翻聊天记录体面得多。 本文为机制说明,不构成任何投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。