核心存款电路

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

存款

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

承诺方案

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

Pedersen 哈希

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

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

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

Tornado 承诺

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

您的存款票据的原像是这两个值(“无效符”+“秘密”)的串联,从而产生一条长度为 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. 最近的默克尔根

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

    1. 提款接收者的地址

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

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

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

提款证明的其他私人输入为:

    1. 来自存款承诺的无效化组件

    1. 来自存款承诺的秘密组件

    1. 默克尔树的根节点和叶节点之间的路径中存在的节点标签集

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

已证明的声明

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

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

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

计算见证

无效器哈希校验

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

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

Merkle 树检查

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

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

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

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

这证明您的承诺存在于指定 Merkle 根下的某个路径中。

额外的提款参数检查

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

计算证明

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

完成提款交易

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

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

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

    1. 提供的默克尔根是已知的,使用 100 个根历史记录

    1. 提供的证明有效

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

如果根据公共输入证明有效,则此函数返回 TRUE

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

Last updated