作者:Kixunil

来源:https://gist.github.com/Kixunil/0ddb3a9cdec33342b97431e438252c0a

摘要

本文提出了一种新方案,既能避免地址复用,又能保留地址复用的一些便利,还能保证只需比特币时间链即可复原钱包,而且避免了可见的指纹。本方案的平均开销可以忽略不计。

动机

地址复用(address reuse,重复使用同一个地址)是最破环隐私性的习惯之一。但是,不能否认,地址复用确实很方便,要不然人们也不会这么做。在长期的商务关系中尤其如此,它避免了来回传递新地址的需要。

这个问题现有的解决方案包括:

  • 隐身地址(Stealth Addresses)——他们使用 ECDH(基于椭圆曲线的密钥交换)来为新交易生成新的地址,并将这个一次性公钥放到 OP_RETURN 输出中。结果是,这种方案会由很大的开销,而且有可辨识的痕迹。不过,这个方案已经用在门罗币中。
  • 可复用的支付码(BIP47)—— 稍微优化了隐身地址。虽然这种方案只要求发起一笔特殊的交易来初始化,后续的交易跟普通交易开销相同而且没有可见的痕迹,初始化交易必须在商务活动开展前发生,而且它会产生有毒找零输出(toxic change,应指面额过于微小,占用 UTXO 集合的空间但无法独立使用),可能是很难处理的。初始化交易除了提供方便的隐私性之外没有任何价值。有一种替代方案提出使用 Bitmessage 来发送所需要的数据。这倒是解决了成本和痕迹的问题,但接收方要恢复钱包就不能只靠自己的助记词了。换句话说,用 bitmessage 收到的密钥必须备份,而且每个对手方都会一个一个不同的密钥(因此要备份很多密钥)。此外,它还要求使用另一个并未得到广泛使用的协议。

本文希望能解决 BIP47 的 体积/痕迹 问题,同时保证只需助记词就能恢复钱包,无需对手方在线配合。

结构

初始化交易

类似于 BIP47,本方案也要使用 “通知交易(notification transaction)”。不过,跟 BIP47 不同的是,我们这里的通知交易同时也是一笔 “真实的” 交易。它是建立商业关系的双方在首次使用本方案时要执行的交易。通知交易的发送必须花费下列某种类型的至少一个输入:

  • P2PK
  • P2PKH
  • P2SH-P2WPKH
  • P2WPKH
  • P2TR

发送者必须能够用 Taproot 地址来收款。接收方也必须使用 Taproot 地址来收款。(理论上也可以使用 P2PK,但这是很罕见的,所以会更显眼。)交易的双方可以使用 PayJoin 但不能让第三方加入。

在使用这种方案时,发送者必须用下列算法生成一个找零输出。

  1. 发送者在自己拥有的、属于上述类型的输入中,找出一个索引号最小的输入。这个输入记为 “发送者密钥输入”。
  2. 发送者密钥输入的相关私钥记为 p_sender
  3. 接收者所用的 Tarpoot 地址记为 P_receiver
  4. 计算 shared_secret = SHA256(p_sender * P_receiver) (也就是 ECDH 算法)
  5. 计算 offset = HMAC(shared_secret, CHANGE_KEY_CONSTANT) 其中 CHANGE_KEY_CONSTANT 是由协议定义的任意常量
  6. 计算 P_change = (offset + p_sender) * G
  7. 计算并安全地缓存 relationship_seed = HMAC(shared_secret, RELATIONSHIP_SEED_CONSTANT) ,其中 RELATIONSHIP_SEED_CONSTANT 是由协议定义的任意常量,不能等于 CHANGE_KEY_CONSTANT
  8. 在找零输出脚本中使用 P_change

接收者根据下列检查来观测收款地址并响应收到的交易:

  1. 接收者在不属于接收者的、属于上述类型的输入中,找出一个索引号最小的输入 —— 也就是 “发送者密钥输入”
  2. P_sender 是发送者密钥输入的相关 公钥
  3. p_receiver 是接收者所用的 Taproot 地址的私钥
  4. 计算 shared_secret = SHA256(P_sender * p_receiver) (也就是 ECDH 算法)
  5. 计算 offset = HMAC(shared_secret, CHANGE_KEY_CONSTANT)
  6. 计算 P_change = offset * G + P_sender
  7. 检查 P_change 与发送者在找零输出中使用的公钥相匹配
  8. 如果不匹配,不再继续
  9. 计算 relationship_seed = HMAC(shared_secret, RELATIONSHIP_SEED_CONSTANT) 并安全地缓存
  10. 使用一个 密码学安全的伪随机数生成器,输入 relationship_seed,预先计算出足够多数量的 offset
  11. 为每一个 offset 计算 P_i = (offset_i + p_receiver) * G
  12. 开始监控链上有无发给上述公钥的交易

重复发送

每次发送者想要给接收者支付时,就计算 P_i = offset_i * G + P_receiver ,其中 offset_i 是使用上述步骤 9 相同的伪随机数生成器产生的一个随机 nonce 值。然后发送者将资金发到 P_i 。接收者预先缓存了这些地址和私钥,所以 TA 可以容易地识别出发送来的交易,并在需要的时候花费其中的资金。

钱包复原

发送者

在数据丢失的情形中,发送者可以使用这一套算法来复原交易历史、资金和关系:

  1. 用 BIP32 地址扫描所有的交易
  2. 每次发现一笔交易包含了至少两个 Taproot 输出,而且它们不匹配任何 BIP32 地址时,执行下列操作
  3. 为每个输出重复上述推导 shared_secret 和找零输出的算法,并检查推到出的找零输出是否与交易中的某个输出相匹配
  4. 如果匹配,则预先计算 offset 和相关的地址,并将它们以及找零输出添加到扫描地址的清单中

接收者

  1. 用 BIP32 地址扫描所有的交易
  2. 每当发现一笔交易包含了至少一个额外的 Taproot 输出时,尝试计算 shared_secret ,并检查预期的找零输出是否与交易的 Taproot 输出相匹配
  3. 如果发现了匹配,就预先计算 offset,并将相应的公钥加入要扫描的地址集合中

结论

如上所示,通知交易看起来就像真实的交易一样。因为外部观察者无法计算 shared_secret (假设 ECDH 是安全的),他们就不知道支付双方使用了这套协议,也无法计算出后续的地址。这套协议像 BIP47 一样优于隐身地址,但除此之外,通知地址还无法被外部观察者识别出来,找零输出也并不必然是有毒的(依然推荐混币),开销也相对较小。

虽然看起来开销好像是 0,但并非如此。使用这套协议的通知交易的开销是:

  • 这个构造要求使用 Taproot 输出,比 P2WPKH 输出要稍微大一些
  • 即使某些时候不需要找零输出(有足够多可用的 UTXO),也不能省去找零输出
  • 使用PayJoin 来开启闪电网络通道和参与其它的合约都变得不可能,至少需要额外的输出
  • 使用找零来开启闪电网络或把找零输出发送给别人也不行
  • 集合多笔通知交易会给每个接收者产生一个找零输出。好的一面是,它可以模拟 Samourai 的 Stonewall,用额外的手续费换得多一些隐私性。

可以说,在多种多样的交易批处理技巧被广泛使用之前,这套协议的开销都相对较小。

Ruben Somsen 评论:

很棒的文章。你把它写出来真的很有意义。因为 Taproot 会直接暴露公钥,所以事情会变得简单很多。

你的协议变种重新引入了最初的隐身地址提议希望避免的问题:必须为区块链上的(几乎)每一笔交易计算一个共享秘密值。因为你不可能知道什么时候会有一个新的交易对手想跟你建立一个共享秘密值,所以你必须永远保持扫描状态。椭圆曲线的乘法并不便宜(尤其在不是跟 G 相乘的时候),但这基本上可以视为运行一个全节点(以及恢复备份)来获得更多隐私性的额外验证成本。

可能更大的问题是,在你的协议中,发送者不得不遵循一个特殊的协议来恢复钱包,这可能会导致人们不想使用它,尤其在他们不怎么在乎隐私性的场合。

但我相信一个更简单的协议就足够了,还可以消除发送者一端的复杂性。Bob(B) 可以不必标记找零输出,而是直接给 Alice 的隐身公钥(A) 支付:把资金发到 A' = hash(b * A) + A。这样发送者就不必制作特殊的找零输出,而且支付可以立即进行,而接收方的成本是一样的,因为现有的协议已经要求了接收者扫描每一笔交易。

我能想到的缺点有 3 个:

  1. 如果 Bob 复用了密钥并发起了第二笔交易,接收的地址就是一样的。这可以通过在共享秘密哈希值中加入更多信息来缓解,比如 Bob 的输入的 txid 或者当前区块高度的 nlock 字段。
  2. 用 CoinJoin 来支付就不可行了,如果要用第一个输入的话。扫描每一个输入而非每一笔交易的第一个输入可以解决这个问题(但成本会更高)。
  3. 除非每个人都配合,否则你可能无法用自己并不完全控制的输出(交易所或者多签名)来给隐身公钥支付。但在你的协议中,这依然是可以做到的(虽然有点麻烦),只需要先建立一个共享秘密值,推导出一个新的地址,然后就可以在别的地方发起支付。

也可以像我上面说的一样,仅发起第一笔支付来保留支付码,然后继续为该支付码使用共享秘密值。但我认为,像上面那样省去支付码的简洁性有许多的优点,而缺点是相对较小的。

不管怎么说,很高兴看到你重新拾起了这个话题。在 Taproot 出现的时候,我一直很想重温这个话题,但完全忘记了。