Midnight dApp 部署前安全检查清单:开发者指南
Midnight Network 通过零知识证明和可编程隐私控制,为 Web3 带来了隐私保护智能合约。但强大的隐私能力也意味着更高的安全要求。本清单帮助你在 dApp 上线前发现常见漏洞。
为什么需要这份清单
Midnight 的 Compact 语言默认保护隐私——所有数据都是私有的,除非你显式调用 disclose()。这意味着:
- 一个错误放置的
disclose()可能永久泄露敏感数据 - Witness 函数在 ZK 电路外运行,可以被操纵
-
ownPublicKey()函数有一个已知漏洞,很多开发者忽略了 - 重放保护需要仔细实现 nonces 和 nullifiers
让我们系统地检查每个安全领域。
部署前检查清单
✅ 1. disclose() 审计——无秘密泄露
disclose() 是将私有数据移动到公开账本的唯一机制。代码中的每个 disclose() 调用都是一个有意识地公开某数据的决定。
需要检查的内容:
// ❌ 错误:过早公开会在后续操作中暴露值
export circuit store(flag: Boolean): [] {
const secret = disclose(getSecret()); // 公开太早了!
const derived = computeValue(secret); // `secret` 现在可见了
result = disclose(flag) ? derived : 0;
}
// ✅ 正确:仅在使用点公开
export circuit store(flag: Boolean): [] {
const secret = getSecret();
const derived = computeValue(secret); // 仍然私有
result = disclose(flag) ? disclose(derived) : 0; // 具体、有针对性的公开
}
检查项:
- [ ] 搜索代码库中的每个
disclose()调用 - [ ] 验证每次公开都是有意且最小化的
- [ ] 确保
disclose()尽可能靠近使用点 - [ ] 检查没有代码路径意外公开应保持私有的值
- [ ] 对必须保持私有的值使用下划线前缀(
_sk、_secret)
专业提示: Compact 编译器会捕获将私有值赋给账本字段而没有 disclose() 的尝试,但不会捕获过早的公开。手动审查是必须的。
✅ 2. ownPublicKey() 使用审查(已知漏洞)
这是 Midnight 开发中最常见的安全错误之一。
⚠️ 警告:不要使用 ownPublicKey() 进行调用者验证!
ownPublicKey() 技术上是一个 witness 函数。这意味着每个用户的前端都可以产生恶意的返回值。攻击者可以修改本地 witness 实现来返回任意公钥。
// ❌ 严重漏洞:使用 ownPublicKey() 进行授权
export circuit transfer(to: Bytes<32>, amount: Uint<64>): [] {
const sender = ownPublicKey(); // 攻击者可以返回任何密钥!
assert(balances.member(sender), "No balance");
// ... 使用伪造的发送者进行转账逻辑
}
// ✅ 正确:基于哈希的身份验证
witness secretKey(): Bytes<32>;
circuit publicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "midnight:auth:pk"),
_sk
]);
}
export circuit authorizedOperation(newValue: Bytes<32>): [] {
const _sk = secretKey();
const pk = publicKey(_sk);
assert(disclose(pk) == authority, "Authorization failed");
ledgerValue = disclose(newValue);
}
检查项:
- [ ] 搜索代码库中所有
ownPublicKey()调用 - [ ] 验证没有一个被用作唯一的授权机制
- [ ] 将任何授权逻辑替换为基于哈希的模式
- [ ] 如果必须使用
ownPublicKey(),添加额外的验证层
✅ 3. 重放保护验证(Nonces 和 Nullifiers)
Midnight 使用承诺/空值器模式(来自 Zerocash/Zswap)来防止双重花费,而不暴露哪个资源被花费。
空值器工作原理:
export ledger usedNullifiers: Set<Bytes<32>>;
circuit nullifier(secretKey: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "nullifier-domain"), // 域分隔符!
secretKey
]);
}
export circuit spend(secretKey: Bytes<32>): [] {
const nul = nullifier(secretKey);
assert(!usedNullifiers.member(nul), "Already spent");
usedNullifiers.insert(disclose(nul));
}
关键要求:
- [ ] 域分隔符:承诺和空值器必须使用不同的域前缀,以防止哈希碰撞攻击
- 好的:
"nullifier-my-dapp-v1"vs"commitment-my-dapp-v1" - 坏的:对两者使用相同的前缀
- 好的:
- [ ] 不重用随机性:永远不要在承诺之间重用随机性——这会启用链接并破坏隐私
- [ ] 使用
persistentHash:用于存储在账本状态中的值(保证升级后一致) - [ ] 回合计数器:为操作包含时间绑定计数器,防止旧证明的重放
✅ 4. 导出账本字段审查
每个 export ledger 字段在链上公开可见。审查每个字段:
export ledger balance: Counter;
export ledger authority: Bytes<32>;
export ledger usedNullifiers: Set<Bytes<32>>;
检查项:
- [ ] 验证每个账本字段确实需要公开
- [ ] 检查没有私有数据意外存储在账本字段中
- [ ] 确保账本字段类型匹配其预期用途
- [ ] 审查访问模式——谁可以读取和修改每个字段?
✅ 5. Witness 实现正确性
Witness 是链下 TypeScript 函数,在 ZK 电路外运行。它们不被加密验证。
// TypeScript 中的 Witness 实现
export const witnesses = {
secretKey: ({ privateState }: WitnessContext<Ledger, PrivateState>) =>
[privateState, privateState.secretKey],
getUserBalance: ({ privateState }: WitnessContext<Ledger, PrivateState>) =>
[privateState, privateState.balance],
};
安全影响:
- 每个用户提供自己的 witness 实现
- 攻击者可以从 witness 函数返回任何值
- 合约逻辑永远不能信任 witness 值而不验证
检查项:
- [ ] 每个 witness 输出在电路中使用前都用
assert验证 - [ ] 没有关键逻辑仅依赖 witness 返回值
- [ ] Witness 函数返回一致的
[PrivateState, ReturnValue]元组 - [ ] Witness 中没有可能在调用之间变化的外部依赖
- [ ] 测试 witness 实现的多次调用一致性
✅ 6. 版本兼容性确认
Midnight 的工具链发展迅速。部署前:
检查项:
- [ ] 验证
pragma language_version与安装的 Compact 编译器匹配 - [ ] 检查所有依赖(midnight-mcp、proof-server)版本兼容
- [ ] 查看 Midnight 更新日志 了解破坏性变更
- [ ] 使用合约中指定的确切版本测试编译
✅ 7. 测试网上证明生成测试
在主网之前,验证你的证明生成管道端到端工作。
测试网层级:
| 层级 | 目的 | 代币 |
|------|------|------|
| 本地开发网 | 开发 | 无限 |
| 预览网 | 集成测试 | 测试 NIGHT |
| 预生产网 | 最终验证 | 预生产水龙头 |
检查项:
- [ ] 部署合约到本地开发网并验证所有电路
- [ ] 测试每个电路路径的证明生成
- [ ] 验证 proof-server 处理并发请求
- [ ] 测试边界情况:零值、最大值、空输入
- [ ] 运行完整交易生命周期:部署→交互→验证状态
- [ ] 在预生产网测试后再上主网
Proof 服务器验证:
# 检查 proof 服务器是否运行
curl http://localhost:6300/health
# 测试证明生成
compact prove --contract ./managed/my-contract \
--circuit myCircuit \
--inputs ./test-inputs.json
常见安全陷阱
陷阱 1:信任前端输入
永远不要信任来自前端的数据而不进行链上验证。用户控制他们的客户端。
陷阱 2:缺少域分隔
始终为不同的哈希操作使用唯一的域分隔符:
// 好:不同目的使用不同域
persistentHash<Vector<2, Bytes<32>>>([pad(32, "commitment-v1"), data])
persistentHash<Vector<2, Bytes<32>>>([pad(32, "nullifier-v1"), data])
陷阱 3:过早公开
将 disclose() 尽可能晚地放在计算链中。
陷阱 4:未验证的 Witness 数据
在电路逻辑中使用 witness 输出之前,始终进行 assert。
陷阱 5:忽略可链接性
当隐私重要时,使用回合计数器或其他机制打破交易可链接性。
安全模式快速参考
| 模式 | 用例 | 关键点 |
|---|---|---|
| 基于哈希的认证 | 调用者验证 | 不要单独使用 ownPublicKey()
|
| 承诺-公开 | 密封投标、拍卖 | 两阶段承诺 |
| 域分隔符 | 所有哈希操作 | 每个目的唯一前缀 |
| 回合计数器 | 打破可链接性 | 使旧密钥失效 |
| 延迟公开 | 所有 disclose() 调用 |
最小化暴露窗口 |
资源
总结
部署 Midnight dApp 前,验证:
- ✅ disclose() 审计——每次公开都是有意且最小化的
- ✅ ownPublicKey() 审查——没有依赖它进行授权
- ✅ 重放保护——正确的 nonces、nullifiers 和域分隔符
- ✅ 账本字段——没有意外公开私有数据
- ✅ Witness 验证——所有 witness 输出在使用前都经过断言
- ✅ 版本兼容性——编译器和依赖对齐
- ✅ 测试网验证——完整的证明生成管道已测试
隐私保护 dApp 功能强大,但需要额外的细致工作。每次部署前运行此清单,你的用户数据将保持安全。
📌 免责声明: 本指南仅供教育目的。请随时关注最新的 Midnight Network 文档和安全公告。安全领域变化迅速。
觉得有帮助?关注更多 Web3 安全内容。有问题?在下方评论。
Top comments (0)