作者:almkglor

来源:https://www.reddit.com/r/Bitcoin/comments/ibcnsv/taproot_coinjoins_and_crossinput_signature/

原文撰写于 2020 年 8 月 17 日;翻译之时,Taproot 升级已经激活。

有一种常见的误解是,即将到来的 Taproot 升级可以帮助 CoinJoin 提高隐私性。

摘要:即将到来的 Taproot 升级完全无法帮助等面额输出的 CoinJoin 交易,虽然它有可能可以提高其它协议的的隐私性,比如闪电网络和托管合约。

如果你想了解详情,那就读下去!

等面额输出的 CoinJoin

我们先来讲讲等面额输出的 CoinJoin,这是 JoinMarket 和 Wasabi 钱包使用的 CoinJoin 类型。它的流程第一步是,一些参与者先集体同意一个公共值。在 JoinMarket 中,由 taker 来定义这个值,并给 maker 支付来取得他们的同意;而在 Wasabi 中,服务器会定义一个大致等于 0.1 BTC 的数值。

然后,每个参与者都提供自己单方面控制的输入,每个人提供的输入总面额都要等于或大于公共值。一般来说,因为每个输入都是单方面控制的,所以每个输入都需要一个单签名。每个参与者也都提供自己控制的两个地址,一个地址会得到面额为公共值的支付;另一个则用来接收其所提供的输入中大于公共值的部分(也即找零输出)。

再然后,这组参与者共同生成一笔交易,该交易花费所有参与者提供的输出,并支付给合适的输出。输入和输出都以一些安全的模式混淆一通。然后这笔伪签名的交易又发回给每个参与者。

最后,每个参与者检查这笔交易花费了自己提供的输入(更重要的是它 没有 花费自己 不想 提供的其它资金!)以及支付给了合适的地址。验证完了之后,每个人都为自己提供的输入补充签名。

每个人都为自己约定的所有输入提供签名之后,该交易就有了完整的签名,这笔 CoinJoin 就是有效的,可以打包上链了。

CoinJoin 原理上很简单但有直接的隐私提升效果,它不需要脚本编程,它只需要每个人都提供签名。

隐私性

假设我们有两个参与者,同意加入一个公共值为 0.1 BTC 的 CoinJoin 交易。甲提供了一个面额为 0.105 的输入,乙提供了一个面额为 0.114 的输入。结果是,该 CoinJoin 交易有两个输入,面额分别为 0.105、0.114,而输出则是 0.1、0.005、0.014 以及 0.1 BTC.

现在,很显然这个 0.005 的输出是从 0.105 的输入中来的,而 0.014 的输出是从 0.114 的输入中来的。

但两个 0.1 BTC 的输出无法断言是从哪个输入中来的。没有能够关联起来的信息,因为两个输出都从两个输入中来。这就是常见的 CoinJoin 实现,比如 Wasabi 和 JoinMarket 如何提高隐私性的。

禁止 CoinJoin

不走运的是,Wasabi 和 JoinMarket 的模式下,大规模的 CoinJoin 是非常显眼的。

你只要看到一笔交易有(比如)超过 3 个相等面额的输出,而输入的数量等于或大于等面额输出的数量,那它大概率就是 CoinJoin 交易。因此,很容易找出 Wasabi 和 JoinMarket 做的等面额输出 CoinJoin。甚至你可以很容易地区分它们:Wasabi 的等面额输出 CoinJoin 可能有多于 100 个输入,而输出的面额在 0.1 BTC 左右;JoinMarket 的等面额输出数量一般小于 10 个(通常在 4 ~ 6 个),但公共值变化很大,低至 0.001 BTC,高至几个比特币甚至更多。

这导致了一些反隐私的交易所拒绝为托管账户入账,如果存入的资金上溯一定步数会遇到等面额 CoinJoin 交易的话,通常是以监管上的考虑为理由。致命的是,这些交易所会继续持有那些 “被禁止” 的存款的私钥,所以也可以花费它们,本质上这与盗窃无异。如果你的交易所对你做了这样的事,你应该曝光该交易所从客户手上盗窃资金。此外,谨记,无私钥,即无币。

因此,CoinJoin 表现为隐私上的一种取舍:

  • 其他人很难确定哪个输出来源于哪个输入
  • 对于其他人来说,某个输出是从一个混币操作中出来的,这无可隐瞒

Taproot

现在我们来讨论一下叫做 “Taproot” 的新奇有趣事物。

Taproot 包含两个部分:

  • 使用了基于 Schnorr 的签名方案,支持多签名。使用 Schnorr 公钥来花费的方法被称为 “密钥路径”。
  • 可以秘密地承诺一组脚本,在需要时暴露其中一个并提供正确的输入来花费资金。以隐藏的脚本来花费的方法被称为 “脚本路径”。

它有一些很棒的属性:

  • 直接的多签名支持,意味着所有的签名看起来都一个样。在当前的比特币中,一个 2-2 的 “多签名” 实际上是一个脚本,它要求提供预先指定的两个不同公钥的签名。而对密码学家来说,严格意义上的多签名是 一个 由多方共同创建的签名。
    • 一般来说,最小的 “多签名” 设置就是 2-3 多签,这样即使你弄丢了一个签名设备,也还控制着你的资金;相比单签名,它还提供了额外的安全性,因为 2-3 的多签要求窃贼拿走至少两个签名设备。在当前的比特币中,一个 2-3 的多签是一个包含 3 个公钥的脚本,它要求提供这 3 个公钥中任意 2 个的签名。
    • 但是一个闪电网络通道有两名参与者。因此,它使用了 2-2 的多签名,也即是一个包含 2 个公钥的脚本,需要提供这两个公钥的签名才能解锁。如果你在闪电网络开始流行之后的区块链上寻找 2-2 的花费,你随机看到的 2-2 花费行为有很大的概率是闪电网络通道关闭的交易,因为除此之外很少有什么地方是用到 2-2 多签名的。
    • 因此,你可以很容易地区分最常见的囤币者的 2-3 多签名(包含 3 个公钥)和闪电网络通道(2-2 多签名,包含 2 个公钥)。
    • 幸运的是,有了 Taproot,2-3 和 2-2 的多签名(以及任意的 k-n 多签名)都可以长得一模一样,因为 Schnorr 允许密码学家严格定义的 “多签名”:由多方共同生成的单个签名。
  • 复杂的脚本,比如哈希时间锁合约(HTLC),可以隐藏到一个 Taproot 输出里面。
    • 举个例子,一个输出可以有一条密钥花费路径,是所有参与者 n-n 共同花费的;还可以用隐藏的脚本来编码该输出可以被花费的其它条件
    • 这些隐藏的脚本保证了这 n 个参与者会遵守协议。如果某个参与者背弃了协议,其余人可以揭示隐藏的脚本并根据这些条件把钱转走
    • 如果每个人都合理遵守协议,并一致同意结果,他们可以一起使用 n-n 的签名(密钥路径)来花费资金。他们可以一致同意结果,并签名一条实现该结果的交易,而无需公开任何脚本。因为所有人都同意这个结果,应该没有人会抱怨(如果其中一个认为这个结果是不合理的,TA 可以拒绝签名,迫使其他人把脚本公开在链上并据此花费)。
    • 只要每个人都同意,他们就可以得到隐私性:他们遵循的任何脚本都不会暴露在链上,而且花费行为跟其他人看起来没有区别。

Taproot 无法帮助 CoinJoin

来回顾一下!

CoinJoin:

  • CoinJoin 的输入是单签名
  • CoinJoin 没有用到脚本

Taproot:

  • 提升了多签名的隐私性
  • 提升了脚本的隐私性

完全没有交集。Taproot 帮助的是 CoinJoin 不会用到的东西。CoinJoin 用到的是 Taproot 无法提升的东西。

但有人这么说了!

许多对 Taproot 的早期介绍都声称 Taproot 有益于 CoinJoin。

让他们搞混的地方在于,早期的 Taproot 草案包含一个叫做 “跨输入的签名聚合” 的功能。

在当前的比特币上,每一个输入,在花费时,都需要独立的签名。而有了跨输入的签名聚合,所有支持这个特性的输入可以用一个签名来覆盖。所以,举个例子,如果你想花费两个输入,当前的比特币协议需要你为每个输入提供一个签名,但有了跨输入的签名聚合,你可以用一个签名来解锁这两个输入。甚至即使输入是属于不同公钥的也可以:两个输入的跨输入签名聚合本质上是定义了一个 2-2 的公钥,而你只能在知道这两个输入的私钥时才能给它们签名;或者你能跟其他知道相应输入的私钥的人合作也可以。

它才能帮助 CoinJoin 节省成本。因为 CoinJoin 会有很多输入(每个参与者都会提供至少一个输入,有时会更多;而参与者集合越大,CoinJoin 的隐私性就越强),如果所有输入都启用了跨输入的签名聚合,这样大型的 CoinJoin 也可以只用一个签名。

它会让 CoinJoin 的签名流程变得更复杂(签名者现在要合作签名),但这也是值得的,它可以减少签名的体积,因此降低手续费。

但要注意,虽然跨输入的签名聚合提高了 CoinJoin 的经济性,它并不能提升隐私性!等面额输出的 CoinJoin 仍然是非常显眼的,仍然很容易被憎恨隐私性的交易所禁止。它不能提高 CoinJoin 的隐私性。另外,见:https://old.reddit.com/r/Bitcoin/comments/gqb3ur/design_for_a_coinswap_implementation_for/

为什么现在不加上跨输入的签名聚合?

有一些非常复杂的技术理由导致跨输入的签名聚合不在最新的 Taproot 提议中。

首要的理由是降低 Taproot 的技术复杂性,希望这样能更容易说服用户激活 Taproot(虽然 Taproot 的呼声很高,但考虑到之前隔离见证(SegWit)遇到的困难,开发者对新的提议能否激活持谨慎态度)。

这里主要的技术复杂性在于,它会跟未来增强比特币的方法相互作用。

本文后面的部分假设你已经知道了比特币的脚本是怎么工作的。如果你并不深入了解比特币脚本的工作原理,那简而言之,跨输入的签名聚合会导致未来增强比特币的方法变得更加复杂,所以大家推迟了它,好让开发者有更多的时间思考。

(下文是我的理解;可能 /u/pwuille/u/ajtowns 可以给出更好的总结。)

详细说来,Taproot 也引入了 OP_SUCCESS 操作码。如果你知道当前的比特币协议已经定义的 OP_NOP 操作码,那么,你可以认为 OP_SUCCESS 就是 “传出通过的 OP_NOP”。

现在,OP_NOP 是一个什么也不干的操作。在未来版本的比特币中,它可以被替换成检查某些条件的操作,并在条件不满足时传出失败。举个例子,OP_CHECKLOCKTIMEVERIFYOP_CHECKSEQUENCEVERIFY 之前都是 OP_NOP 操作码(译者注:两个都是脚本层面的时间锁)。更老的节点看到一个 OP_CHECKLOCKTIMEVERIFY 会认为这个操作码不会产生任何操作,但更新的节点会检查 nLockTime 字段是否有一个正确指定的数值,并在条件不满足时传出失败。因为网络中大部分的节点都在使用新得多的节点软件,更老的节点就受到了保护,不会被尝试盗用 OP_CHECKLOCKTIMEVERIFY/OP_CHECKSEQUENCEVERIFY 输出的人蒙骗;而这些更老的节点也依然能够跟网络中的其他人保持同步:有赖于一个共识系统必需的严格向后兼容性。

软分叉基本上意味着:如果一个脚本在最新版本的软件中会处理通过,那在所有旧版本中也必须通过;不能在新版本可以通过、同时在旧版本中却会导致失败,因为这会使网络内产生分歧,会将旧节点踢出网络(即,那就变成一个硬分叉了)。

OP_NOP 是一种非常有限的添加新操作码的方法。替代 OP_NOP 的操作码只能做一件事:检查某些条件是否为真。它们无法为栈推入新数据,也无法让数据从栈中弹出。举个例子,假设我们想加入一个 OP_GETBLOCKHEIGHT 操作码而不是 OP_CHECKLOCKTIMEVERIFY。这个操作码将读取区块链的高度并将该数据推入栈中。如果这个命令提到了一个更老的 OP_NOP 操作码,那么一个像 OP_GETBLOCKHEIGHT 650000 OP_EQUAL 这样的脚本在未来的比特币版本中可能通过,但更老的版本在执行 OP_NOP 650000 OP_EQUAL 时却会失败,因为 OP_EQUAL 预期栈中会有两个元素(但 NOP 无法推入元素,于是栈中只有一个元素)。那么,旧的版本在执行时会失败,更新的版本却会成功,这就成了一个硬分叉,变成了后向不兼容的了。

OP_SUCCESS 有所不同。首先,旧节点在解析脚本时,一旦看到 OP_SUCCESS ,就不会执行脚本主体,而会认为这个脚本已经通过了。所以, OP_GETBLOCKHEIGHT 650000 OP_EQUAL 这个脚本在此时就不会造成问题了:新版本的比特币会正常执行并通过,而无法理解 OP_GETBLOCKHEIGHT 650000 OP_EQUAL 的旧节点会看到 OP_SUCCESS 650000 OP_EQUAL ,然后完全不执行这个脚本、立即通过。这样一来,一个在新版本会通过的脚本,在旧版本中也会通过,保持了共识软分叉需要的后向兼容性。

那么, OP_SUCCESS 会给跨输入的签名聚合造成什么困难呢?嗯,要求验证签名的其中一种办法是通过操作码 OP_CHECKSIGVERIFY。有了跨输入的签名聚合之后,如果一个公钥表示自己可以用于跨输入的签名聚合,OP_CHECKSIGVERIFY就不会要求有一个签名在栈中,那么栈在签名位置会包含一个无意义的 0 值,然后这个公钥会被加到一个 “总和” 公钥中(也即,这个总和公钥是一个动态延伸的 n-n 公钥,每执行一次 OP_CHECKSIGVERIFY 就加入一个公钥),这个总和公钥以及那个签名会在最后由跨输入签名聚合验证算法来验证。

这里的重点是,想要把一个公钥加入总和公钥中参与最终签名的检查,就必须执行 OP_CHECKSIGVERIFY

但你记得吗, OP_SUCCESS 会阻止执行!在一个脚本解析完之后,只要有一个操作码是 OP_SUCCESS ,旧节点就会认为这个脚本可以通过,完全不执行这个脚本,因为 OP_SUCCESS 在更新的版本中可能有完全迥异的含义,而当前的版本不应对此有任何假设。如果一个脚本在 OP_SUCCESS 之外 还有 OP_CHECKSIGVERIFY指令,这个指令在当前的版本中也不会执行,因此也无法把 OP_CHECKSIGVERIFY 所提供的公钥加到总和公钥中。更新的版本也必须接受这一点:如果他们解析出的 OP_SUCCESS 有一个新的含义,然后他们执行了该脚本中的一个 OP_CHECKSIGVERIFY ,他们也无法加总出一个跟旧节点相同的总和公钥,因为旧节点根本看不见这些公钥。这就意味着,如果未来有某些操作码替换了一些 OP_SUCCESS 操作码,你可能需要多于一个签名。

因此,因为实现跨输入的签名聚合并保证其与未来协议插件相兼容的复杂性,跨输入的签名聚合被推迟了。

(完)