核心存款电路

核心存款电路是大多数用户交互的,证明用户已经创建了代表存入某些相应资产面额的承诺,他们尚未提取该资产,并且他们知道在生成初始承诺时提供的秘密。

存款

向 Tornado.cash 存款是一个非常简单的操作,实际上不涉及任何 ZK 证明。至少现在还不需要。要存款,您需要调用Tornado 合约deposit实例的方法,提供 Pedersen 承诺以及您要存入的资产面额。此承诺被插入到专门的 Merkle 树中,其中 Merkle 树的结构与与 BN128 椭圆曲线顺序中的素数相关联的椭圆曲线对齐,并且使用 MiMC 哈希计算树的标签。

承诺计划

在密码学中,当你做出“承诺”时,你所做的就是获取一个秘密值(通常很大且随机),并通过一些加密函数(例如哈希函数)运行它,然后披露结果。稍后,当您需要履行承诺时,您要证明您知道原始秘密值。

佩德森哈希

Pedersen 哈希是一种非常专业的哈希函数,特别适合用于利用零知识证明电路的应用程序。其他哈希函数(如 SHA-256)被设计为具有一些特性,例如即使输入稍有不同,也会产生非常不同的输出(雪崩效应),而 Pedersen 哈希则优先考虑在零知识电路中极其高效地计算哈希的能力。

使用 Pedersen 对消息进行哈希处理会将消息的位压缩到一条椭圆曲线上的一个点,该曲线称为 Baby Jubjub。Baby Jubjub 的阶数与 BN128 椭圆曲线相同,该曲线由以太坊网络上的预编译操作支持,这些操作是在 EIP-196 中添加的。这意味着使用 Baby Jubjub 曲线的操作(例如 Pedersen 哈希处理)非常节省 gas。

当您计算消息的 Pedersen 哈希值时,沿其椭圆曲线的结果点非常容易验证,但无法反转回原始消息。

龙卷风承诺

要生成 Tornado.cash 存款承诺,您首先需要生成两个较大的随机整数,每个整数长度为 31 个字节。第一个值是一个无效符,您稍后将披露该值以提取存款,第二个值是一个秘密,用于保护您的存款和取款之间的机密关系。

您的存款票据的原像就是这两个值的串联 ( nullifier+ secret),从而产生一条长度为 62 字节的消息。此消息经过 Pedersen 哈希处理,从而产生一个输出,该输出表示编码为 32 字节大端整数的 Baby Jubjub 椭圆曲线的元素。

如果你想以代码形式查看这一点,你可以参考tornado-cli 存款功能

MiMC Merkle 树

Tornado合约是一种特殊的 Merkle 树,它使用 MiMC 哈希标记其节点。

对于不熟悉 Merkle 树的人来说,它们是二叉树,其中每个非叶节点都标有其子节点标签的哈希值,而叶节点则标有其数据的哈希值。通常,Merkle 树使用单向加密哈希函数(如 SHA-2),但在这种情况下,我们使用 MiMC,它具有一些有用的属性。

MiMC 的一个有用特性是它非常适合在素数域上进行操作,这对我们很重要,因为零知识证明从根本上来说基于素数域,而 Pedersen 哈希是素数域内的点,该域由 Baby Jubjub 椭圆曲线定义 - 而该曲线又在以太坊原生支持的 BN128 曲线的阶内。由于零知识证明在操作上成本高昂,并且以太坊交易中的每个操作都有相应的 gas 成本,因此我们设计的特定类型的操作需要尽可能节省 gas。

MiMC 的其他特别有用的特性是它不可并行化,难以计算但易于验证。这些特性通过使计算在 Merkle 树中具有冲突路径的伪造“承诺”变得不可行,从而增加了合约的安全性。

零节点

在 Tornado Merkle 树初始化期间,会预先分配一条跨越树高的唯一路径,从标签为 的“零叶”节点开始keccak256("tornado") % FIELD_SIZE。然后,每个后续朝向根的非叶节点都会被标记,就好像树的整个底部都由同一个叶节点填充一样。

这些“零节点”的目的是确保 Merkle 树内的所有路径都是无效的,直到它们以有效承诺终止。

插入承诺

当你将承诺插入到 Tornado 合约的 merkle 树中时,你将用一个新叶子替换“零叶子”,该新叶子的标签是你的 Pedersen 承诺的 MiMC 哈希,然后遍历树,并根据新叶子在下面引入的标签更新,用新标签更新每个后续父节点。

承诺在树中从左到右插入,每两个承诺插入填充一棵“子树”。每次插入都会增加树的“索引”,以确定下一个承诺是插入到其 merkle 路径条目的左侧还是右侧。

一旦您的存款更新了树,最顶层节点的标签将成为树的新“根”,并添加到包含最后 100 个根的标签的滚动历史记录中,以供以后用于处理提款交易。

Tornado.cash 存款合约部署了 20 个“级别”,每个级别将潜在叶子的数量增加 2 的幂。这意味着合约的默克尔树最多支持 2^20 个叶子,允许在需要更换合约之前向合约中存入最多 1,048,576 笔存款。

层级数量看似较少的原因是,每笔存款都必须对树进行与层级数量相同的更新。层级越多的树,每笔存款需要的 gas 就越多,提取票据时需要的证明大小也就越大。

提款

存款后,您现在拥有一组真实声明,您可以据此生成证明。一般而言,零知识证明锚定在证明者和验证者都知道的某些值上,这些值与只有证明者知道的一组值之间存在关系。电路验证者可以确认证明者使用了已知的值,并且他们计算的证明满足电路施加的约束。

提款证明的输入

对于 Tornado.cash 存款,证明者(提交提款交易的人)和验证者(存款合约的提款方法)都知道最近的 merkle 根。证明者还提供一组其他公共输入,用于生成证明。

提款证明的公共输入总量如下:

  1. 最近的 Merkle 根

  2. 来自其存款承诺的无效化组件的 Pedersen 哈希

  3. 提款接收者的地址

  4. 他们选择的中继者的地址(或他们自己的地址)

  5. 他们向中继者支付的费用(或零)

  6. 他们向中继者支付的退款(或零)

提款证明的额外私人输入包括:

  1. 存款承诺中的无效部分

  2. 存款承诺的秘密部分

  3. Merkle 树的根节点和叶节点之间的路径中存在的节点标签集合

  4. 一个值数组0/1,指示每个指定路径元素是位于其父节点的左侧还是右侧

已证实的主张

当我们构建承诺并将其插入 Merkle 树时,我们很容易忽略我们创造的巧妙的新知识。您可能倾向于认为,要进行提款,我们只需证明我们知道 Pedersen 承诺的组成部分,而 Merkle 树只是存储这些承诺哈希的一种有效方式。

这种构造的特殊之处在于,它不仅使我们能够证明我们知道已存承诺的组成部分,而且使我们能够简单地证明我们知道树中承诺的路径,以及如何从承诺原像开始到达那里。

如果我们只是证明我们知道已存哈希的原像,那么我们就有可能泄露哪个承诺是我们的。相反,我们不会泄露承诺原像,而是简单地证明我们知道树中承诺的原像。在电路协议的提款端,哪个承诺是我们的完全无法区分。

计算见证

无效哈希校验

为了计算提款证明的见证,我们的电路首先获取私人存款承诺输入 ( nullifier+ secret),并将它们通过电路组件运行,该组件同时计算完整承诺消息的 Pedersen 哈希和无效符的 Pedersen 哈希。然后,电路将生成的无效符哈希与您作​​为公共输入提供的哈希进行比较,并断言它们相等。

这证明您公开提供的无效哈希实际上是您原始承诺的组成部分。

Merkle 树检查

接下来,电路将其计算的承诺哈希、您公开指定的 merkle 根以及您私下指定的路径元素和左/右选择器作为检查您的 merkle 树路径声明的组件的输入。

Merkle Tree Checker 从路径底部开始,将您的承诺哈希和您提议路径的第一个元素输入到 Muxer 中。Muxer 接受第三个输入,即您提供的左/右方向的一个元素。Muxer 组件使用这些方向来通知 MiMC 哈希组件其输入的顺序。如果提供的方向为 0,则提供的路径元素在左侧,而您的承诺哈希在右侧。如果方向为 1,则顺序相反。

MiMC 哈希器输出生成的哈希值,Merkle Tree 检查器继续进行下一级。它会重复上一个过程,只不过这一次它使用的是最后一级的哈希值,而不是您的承诺哈希值。它会继续遍历建议路径的每一级,直到得到最终的哈希值输出。

Merkle 树检查器将其计算出的哈希值与您提供的公共 Merkle 根输入进行比较,并断言它们相等。

这证明你的承诺存在于指定的 merkle 根下的某条路径内。

额外提款参数检查

在完成之前,电路会获取剩余的四个公共输入,并将它们平方为一个公共输出。虽然这不是绝对必要的,但它会在您的证明中创建一组约束,以确保在处理您的提款交易之前,您的交易参数不会被篡改。如果任何这些参数发生变化,您的证明将不再有效。

计算证明

现在我们有了证明的见证人,我们将这些见证的状态值输入到与提款电路相对应的 R1CS 中,并运行证明器。证明器产生两个证明工件。第一个是证明本身(根据我们使用的 SNARK 协议),第二个是与该证明相对应的一组公共输入和输出。

完成提款交易

现在已生成提款证明,您可以将该证明及其公共输入提供给withdraw存款合约的方法。此方法验证:

  1. 指定的中继费不超过提取资产的面额价值

  2. 提供的无效哈希之前未被使用过

  3. 提供的 merkle 根是已知的,使用 100 个根的历史记录

  4. 提供的证明有效

作为存款合约的依赖项部署的工件之一是 Solidity 合约,该合约使用取款电路的证明密钥作为输入生成。此验证器合约是一个经过优化的证明验证器,具有单个公共视图函数,它接受证明和六个公共输入的数组作为uint256值。

此函数返回TRUE根据公众输入证明是否有效。

如果满足上述先决条件,则将提供的无效符哈希插入到已使用无效符的集合中,然后根据指定的费用参数在接收者和中继者之间分配押金的价值。

最后更新于