想象一下:你想参与一个代币预售,项目方要求你提交钱包地址以进入白名单,于是你的持仓、链上行为全暴露了。你参加一场线上投票,系统记录下你投给了谁,从此你的政治倾向被永久刻在链上。你每天刷门禁卡进公司,几点几分进入哪个房间,后台一清二楚。所有会员系统几乎都在做同一笔交易——想要证明自己“属于这里”,就得先交出“你是谁”。这笔交易,我们做了几十年,直到零知识证明把它彻底撕碎。

Midnight的Compact语言硬核教程里,就藏着这个颠覆性的方案:一个合约,同时处理白名单、投票资格、门禁权限,而且全都不暴露具体成员身份。同样的电路,底层都是同一个模式——用一棵默克尔树和几个哈希,就把“证明资格”和“泄露身份”彻底解耦。下面我们逐条拆解,看看它是怎么把传统系统的老底掀翻的。

第一条:传统系统“要证就露底”的困局,该结束了。
不管是代币销售的允许名单、投票人登记册,还是企业门禁系统,设计逻辑都惊人一致:系统必须知道你是谁,才能确认你有资格。所以你的地址被公开,你的投票选择被记录,你的行动轨迹被追踪。这根本不是隐私问题,这是架构问题——从根上,这些系统就没想过“匿名验证”是可以实现的。Midnight的合约直接打破这个取舍,用零知识电路让成员可以向链上证明“我在名单里”,但不必指名道姓是哪一个。

第二条:一棵能装百万人的“历史树”,才是核心骨架。
合约采用HistoricMerkleTree<20, Bytes<32>>,深度20,最多能容纳1,048,576个成员。每个叶子节点是三个值的哈希:一个域标签、一个本地生成的秘密、一个随机数(nonce)。管理员只负责把叶子哈希上链,成员的秘密和nonce自己保管,从不共享。等到要证明时,成员把秘密和nonce作为私有见证输入电路,电路重新计算出叶子哈希,再验证这叶子确实在树里。不知道秘密?那就不可能伪造证明。

第三条:一个nonce,既防暴力破解,又让一秘多用。
秘密是成员本地随机生成的32字节值,理论上可以和密码一样长。但密码还可能被暴力撞库,于是合约加了一个nonce——一个随机数。即使有人选了短秘密,nonce一混入哈希,输出就完全不可预测。nonce还顺便解决了一个工程需要:同一个秘密配合不同的nonce,就能多次注册,而叶子哈希不同,系统不会当成重复。

第四条:为什么不用普通默克尔树?因为每次加人,所有人的证明都会失效。
这是合约设计中最大的坑,也是Midnight最狠的一手。如果只存一个当前树根(比如Bytes<32>字段),那每次新成员注册,根就变一次。假设Alice在根R1时生成了自己的包含证明,管理员随后注册Bob,根变成R2,Alice的路径还指向R1,但链上只有R2,证明直接失败。不是因为Alice资格被取消,而是根在她脚下移动了。这意味着,只要系统还在吸纳新成员,所有人的证明就得不停重算,毫无实用价值。
HistoricMerkleTree的设计彻底解决了这个问题:它保留树曾有过的每一个历史根,验证方法checkRoot接受任何历史根,而不仅是当前根。所以Alice针对R1生成的路径,在R2、R3乃至以后的任何根下继续有效。这才是生产级注册表该有的“管理员只加人,用户不受影响”的安静体验。

第五条:合约代码没几行,但把“防双花”和计数器安排得明明白白。
看精简后的合约声明:

export ledger memberTree: HistoricMerkleTree<20, Bytes<32>>;
export ledger usedNullifiers: Map, Boolean>;
export ledger memberCount: Counter;

因为没有Set类型,就用Map, Boolean>来标记nullifier是否已被消费——这正是防止同一证明重复使用的关键。memberCount计数器记录总注册人数。整套逻辑加起来不到半页代码,却支撑起一个不泄露身份、可无限扩展、历史证明可追溯的白名单系统。那些还在用传统架构收据存根的公司,看见这个估计要重新想想自己的数据库账单了。