Silent Payments and Alternatives
演讲者: Ruben Somsen
日期: October 15, 2022
记录者: Bryan Bishop
译者: Ajian
标签: Privacy enhancements, Silent payments
类别: Core dev tech
引言
我希望谈谈 “静默支付”,以及让你可以非交互地给他人支付的多种构造。在比特币的世界里,有许多常见的支付方式,发起支付是稀松平常的事。我们当前拥有的另类选择是,你可以生成一个地址然后放到你的 Twitter 介绍里,然后所有人都可以直接给你支付了,但这就没有隐私。所以,要么你需要在接收支付时跟发送者互动,要么你可以持续复用相同的地址,但要接受隐私性上的牺牲。
本演讲准备探究一下我们如何可以既做到非交互性,又能保留隐私性,也就是其他人无法在链上看到所有给我们的支付、知晓所有支付给同一个地址的资金。
出于这个目的,人们已经提出了很多协议了,但也有一些新出现的协议。我会展示其中的可能性。
给出一个 xpub 公钥
有一些方案我称之为 “准-交互式的解决方案”,意思是它们做不到无需交互,但交互量已经尽可能小了。其中一种非常基础的方案是,在你想给某人支付时,对方不是给你一个地址,而是给你一个新的 xpub 公钥,或者一个你可以从中生成许多地址的公钥。这样,你就不必在每次想要给这人支付时都请求一个新地址,只需最开始的一次交互就可以了(收款的地址你可以自己生成出来)。现在没有人会这样做,但我认为在有些时候,这样做也是合理的。对我来说,这样做会让事情变得简单一些。我遇到过好多次某个朋友想要跟我来回支付的情形,这时候获取新地址就会变成一个麻烦,因为他们可能需要回家拿到硬件钱包、生成一个新的地址,再交给我。如果只需交互一次,看起来会好一些。
这种方案的一个问题是(在恢复钱包时)会触发钱包的 “间隔限制(gap limit)” 。(译者注:你需要先理解 BIP32 才能理解这个问题。钱包遵循 BIP32,使用单个种子词根据递进的路径推导出数量没有上限的地址,从钱包内部来看,这些地址是 “连续的”。如果你在使用这些地址时没有严格保证逐个使用,就会在用过的地址之间出现 “间隔” —— 由没有用过的地址形成的队列。)因为你给出的 xpub 之间可能也有间隔,这个问题会比在交互式支付(你可以决定使用哪个地址)中更加严重。问题是这样的:如果你要持续给出地址,钱包工作的方式是从单个公钥为每次支付生成一个新地址;当你要从备份中恢复钱包时,你可以重复生成你可能用过的地址,然后到链上去,看看哪些地址已经用过了、带有余额;当钱包发现没有用过的地址的队列达到一定的长度时,就会假设扫描可以到此为止了(即假设剩下的将全部都是没有用过的地址,不可能具有余额),以免无穷无尽地扫描下去(译者注:也就是说,实际上,钱包不允许两个用过的地址之间出现太长的间隔)。但是,要是你给出了许许多多个地址,都没有被用过呢?当然,你可以暴力穷举,保证自己不会漏掉任何资金,但这是非常丑陋的。
在给出 xpub 的方案中,因为你给出的是一个 xpub,不同的 xpub 之间可能有未使用的,而每个 xpub 所推导的地址中也可能出现间隔(所以问题会更加严重)。一个观察是,如果你给了某人一个 xpub,当然在这个 xpub 推导出的地址中可以会有连续的未使用的地址,但是,请考虑一下,如果你可以假设持续给你支付的是同一个人,那就不会有很长的空白,因为没有理由需要留下很长的空白。还有一个假设是,你自己可以设法减少 xpub 之间的空白,从而补救这种情形。不同的 xpub 之间当然有空白,这取决于你是怎么给出这些 xpub 的。
直接给出 xpub 的方法对循环支付友好,但依然需要交互。依然需要一次交互。
自动化交互
另一类解决方案我称之为 “自动化交互”,就就是你运行一个服务端,然后服务器会帮你给出地址。这种方案需要一个安全的、始终在线的服务端,但在现实中,用户很难满足这种要求。这个房间里的开发者可以拍胸脯保证说这没什么难得,但普通用户会不得其门而入。
还有另一个严重的问题:依然难以保证未使用的地址不会形成长串。如果你设置了一个自动化的服务端,某些人可能会一直访问你的服务器并一直请求公钥。这很容易就会触发钱包的长度限制。所以,这个方案也没有解决间隔限制问题。
这种方案现在已经有了,比如 btcpay 服务端。我不知道他们是怎么处理间隔限制问题的。我认为他们会重新扫描一遍。我不知道他们有没有什么好办法,或者我可能漏掉了一些东西。
在现实中,已经有使用 btcpay 服务的商家了,但起码没有得到应该获得的接受程度。
免信任的地址服务器
还有一类我没有讲到的解决方案。我在最近的一篇邮件组帖子中称之为 “免信任的地址服务器”。它类似于 btcpay 服务端,但你不必自己运行服务端,你可以让别人来运行。关于为什么这是可行的,我的观察之一是,我们网络上有大量的轻客户端会给出公钥,有时候是 xpub,有时是自己生成的公钥,有时候把它们发送给一个服务端。面对帮助他们扫描区块链的服务端,他们已经丧失许多隐私性了。
如果我们已经有了一个服务端,并且知道他们可以让我们完全去匿名化并且觉得无所谓,为什么不依赖他们代表我们给出地址呢?
这里我们依然不能解决间隔控制问题。无论你自己运行服务器,还是别人帮助你,都有间隔控制问题。我有一个办法,但我们后面再说。所以,现在,你不是自己运行服务端,是别人运行服务端。一个新的问题出现了:要是服务端代表你给出了一个公钥,想要给你支付的人怎么确保那个公钥属于你呢?这个服务端完全可以尝试从中作梗,让支付者把钱打到他们的地址里去。
答案是,你可以为支付给你的人安排一个公钥,并使用这个公钥签名放在服务端的所有地址。我可以使用这个身份公钥签名我交给服务端的所有公钥(或者说地址)。现在,当某人想要从服务端获得地址时,他们得到的将不仅有地址,还有对地址的签名。这保证了服务端无法骗人,无法给出错误的地址。
在我们已经讲到的方法中,这是一种中庸的解决方案。它跟 btcpay 服务器只有细微的差别。而且其取舍也有趣。具体来说,这种方案假设,一些轻客户端已经在使用一种不完美的 —— 会把你的隐私暴露出去 —— 的方案了,所以也许会接受这种方案。我认为,因为这种隐私暴露,它不会是我们比特币社区想要的情形,但它们已经存在了,所以这种方案的取舍似乎也是合理的。
间隔限制措施滥用的处理
如果人们只是连接到你的服务器、多次请求公钥、别的什么也不做,然后你就找不回你的资金了,因为在你实际用过的两个公钥之间存在太长的间隔(可能超越了钱包间隔限制的数倍),这算什么事?
此处的第一个观察是,你需要让请求地址变成一件有代价的事。理论上来说,你可以要求人们先发送一笔闪电支付,然后才能获得一个地址。但这有点尴尬,因为想要给你支付的人还得先在另一个网络上给你支付一笔钱。虽然这在理论上是可以做到的。
不过,另一种思路是,拥有一个 UTXO,或者说使用一个 UTXO,本身就是一件有成本的事。你必须在链上做过什么需要成本的事,然后才能获得一个 UTXO。我们可以利用这个事实:在你想要支付时,你必须先有一个 UTXO,然后你可以证明你拥有这个 UTXO;为请求新地址添加成本。
在我把免信任地址服务的概念和大体结构发到邮件组之后,David Harding 回复了我,并纠正了我的想法中的一些粗疏的地方。他提出了一些具体的建议,可以优化我的提议,并使之变得非常优雅,而且似乎更加实用。首先,给出一个旧的地址。如果你联系服务端,服务端会给你一个公钥,这个公钥可能之前已经交给过别人。服务器并不保证这是一个新的公钥,可能别人已经得到过它了。希望给你支付的人先创建一笔交易,给这个并非最理想的地址支付,然后把交易交给服务端。服务端先检查这是不是一笔有效的、可以发到区块链上去的交易,仅在交易有效的情况下,才给支付者提供一个新地址。然后,支付者就可以给这个新地址支付,并广播交易到网络中了。如果服务端想的话,可以让请求地址的人承担代价,并且只有承担了代价,才能得到一个新地址,才能广播对新地址的支付。
如果发送者在两件事之间停住了,比如说获得新地址后就不再响应,你可以广播 TA 原本给服务端的那笔支付(发给旧地址的那笔)。你可以说这不是理想的,因为接收者会泄露隐私,资金会发送到一个别人已经看过的地址上。但是,如果发送者希望让这笔支付的匿名性降级,从得到旧地址的那一刻开始他们就已经可以这样做了,现在你只不过是因为他们不再响应而不得不这么做而已。
有趣的是,这种办法也帮助解决了间隔限制问题。假设你要求支付发送到一个旧地址上,那么你可以给出一个刚好出于间隔限制边界上的地址。比如,如果间隔限制是 100,你可以先给出第 100 个公钥(等待对方发来交易之后再给出新的公钥)。如果你不得不广播对方首先给你发来的交易,你也恰好解决了间隔限制问题。
至于复杂性,这个方案要求支付者签名同一笔交易两次。有一些双向通信。当然,支付者还要检查对公钥的签名,而且支付者广播出来的交易必须使用跟发给服务端的交易相同的输入,这样服务端才不能 “重复花费” 发送者的资金。
Btcpay 服务端也可以使用这种方法。这是一种相对比较新的想法,所以我非常乐于知道你们的反馈。
迪菲-赫尔曼密钥分享
非交互式的支付方案都依赖于同一个技巧。我想先指出这种技巧,然后再讨论与此相关的密码学。
举个例子。假如支付的接收者公示了自己的公钥,那么每个人到知道了这个公钥,那么支付的发送者就可以把自己的公钥(以某种方式)交给接收者,然后双方的公钥就可以产生一个共享的秘密值。这个共享秘密值就是一个很大的数,而且只有发送者和接收者才知道。基于椭圆曲线密码学,你可以按照 迪菲-赫尔曼 的方式来实现这种技巧。得到这个共享的秘密值(共享私钥)之后,你需要调整一下它,这样才能让它产生只有接收者能够花费的地址。因为这个共享私钥本身是双方都知道、可以使用的。你需要调整它,才能使得只有接收者能花费它。
我们以大写字母表示椭圆曲线上的点。小写字母表示一个标量。你可以把一个小写字母(私钥)转化为一个大写字母(公钥),椭圆曲线密码学干的就是这个事。你可以乘出来一个乘积 aB,它就等于 bA。外人只能看到 B 和 A,但因为没有什么 BA 和 AB,所以除了发送者和接收者,没有人能计算出这个共享私钥。(译者注:发送者和接收者各自拿自己的私钥乘以对方的公钥,就得到了这个共享秘密值,但其他人无法得到。)
为了获得一个由接受者控制的公钥,发送者需要计算 hash(aB).G + A,这里的 A 就是接收者的公钥。这保证了只有接收者能够从最终的地址的地址中花费。
BIP47:可复用的支付码
BIP47 是一种使用 DH 密钥分享的协议。另一种是 pyanyms。Samurai Wallet 实现了 BIP47,我印象中 Sparrow Wallet 也实现了。所以它是有一些使用量的。不过跟链上交易的平均数量相比,它的使用量非常小。
BIP47 工作的方式是,首先,接收者在链下(比如在 Twitter 或者个人主页上)公开自己的公钥。也就是说,我声称使用这个公钥的就是我,理想情况下,我也会使用 PGP 系统来确保这一点。在实际支付之前,发送者需要给我提示他们所用的公钥。提示的方法是发起一笔链上通知交易(notification transaction)。也就是支付者发送一笔包含着其公钥的交易,而且这个公钥是盲化过的。我猜这取决于具体的实现。在这种情况下,你肯定不希望外人发觉其中的关系 —— 你不想让别人知道谁在给谁支付,即使后来的实际支付不会跟通知交易关联起来,也不希望通知交易暴露信息。所以发送者的公钥需要盲化。支付者把通知交易发送上链之后,接收者就可以看到交易中包含的公钥,并开始检测对应于共享私钥的地址。也就是说,一旦双方确定了共享私钥,接收方就开始监控收账地址。在这里,间隔限制不是一个问题,因为支付者需要发起昂贵的链上通知。理论上,支付者可以滥发交易来轰炸某人,但这种攻击没什么意思,因为成本很高,收效很低。这种协议在一次性支付的场景中效率极其低下,因为支付者先要发送链上通知,然后才能发送资金。这种天生的缺点是由链上通知的要求带来的。
实际上,当前的 BIP47 实现有一个问题:在你创建链上通知时,你用到了自己的某一些输入,这些输入就跟接收者关联起来了。这里就有一些信息泄露。当然,你可以说,不要使用你在别的场合会用到的输入,而且有很多方法可以处理你的输入的隐私池。但我认为,这是 BIP47 的一个大问题。
为什么链上消息会引发争议
通常人们在这里会提出两个大问题。为什么链上消息会引发这么大的争议?其实这里也有不同的意见。有些人认为,如果你想使用比特币来发送链上消息,那也没什么问题;而有一些人则把这称之为浪费。这从何说起呢?最早的意见是,区块链是全局性的,而一个通知只对个人、小一部分人有意义。那就只应该在发送者和接收者之间发生。其他人都不关心这些消息是什么。如果你使用区块链来发送消息,这是在使用区块链来实现只关系到两个人的事情。但是,在区块链上,我们都关心所有的币在任意时刻的归属。我们都同意跟踪 UTXO 集合。但这个消息并不是 UTXO 集合的一部分。我认为,这是一种根本性的哲学分歧,也就是说你可以把消息放到链上,但最好不要这么做。
另一种观点,也是对一些人来说更有吸引力的观点是,区块空间会变得越来越稀缺,在链上发出通知应该要变得更不经济。如果你想用比特币来支付,那你无可避免要用到比特币的区块空间。但,难道就没有别的方式可以让你们沟通信息吗?
更宽泛地说,我们当然有别的选择。链上通知并不是通知的唯一方法。那我们为什么要使用链上通知呢?
这并不能解决问题,但点出了一些需要思考的问题:你并不需要使用比特币区块链。你只需要一个所有人都同意的全局状态。这真的需要用到比特币区块链吗?能不能用 spachain、Litecoin 或者以太坊来发送消息呢?我认为,这可以打消人们的担忧,但并不能完全解决问题,因为不论在哪里,理论上来说,区块空间都是稀缺的。
为什么通知不能在链下发送?
为什么通知必须在链上发送?为什么 Alice 不能直接给 Bob 发送消息、给 Bob 一个公钥?这不好吗?答案是确实不好,但不像你以为的那么明显。假设 Bob 把他的公钥给了 Alice,是通过一条链下消息发送的,而 Alice 完全不知道自己收到了这样一条消息。如果她还不知道自己得到了 Bob 的公钥,Bob 就开始发送资金,这显然不是理想的情形。所以,接收者需要响应 —— 但是,这不就变回了交互式的方案了吗,这哪还需要一种新协议呀?要是交互可以接受的话,Alice 直接给 Bob 一个新的地址就完了呀。
那么,能不能信任一个服务端来转发消息呢?这又给协议加入了托管元素。支付方发送的公钥是接收者接收资金的必要条件,那么服务端就可以勒索接收者:你想知道这个公钥吗?那你得给我一些好处。这就是对手方风险,接收者会失去自己的资金的控制权。
此外还有数据备份的要求。如果 Bob 直接把公钥告诉了 Alice(而不是发送到区块链上),那么 Alice 就要记住这个公钥。Alice 将没法只备份一个种子词,她需要备份每个支付者发给她的公钥。不是说做不到,只是非常复杂。
此处,区块链实现的有趣效果是它保证了通知会送达。它的内在假设是,有许多的矿工,区块会广泛分发,如果你把数据放到了比特币区块链上,那么基本上你可以保证,无论谁需要这个数据,最终都一定能得到这个数据。这一点才是比特币区块链在这套协议中的用处。区块链解决了消息送达的问题,同时还防止了垃圾邮件攻击,因为在链上发送消息是很贵的。
你可以这么理解:Alice 要给 Bob 发送一条消息,她用到了一个第三方,这个第三方就是区块链,是一个非常可靠的第三方。不是很有趣吗?
BIP47 的变种:布拉格协议
我们一直在讨论有没有办法可以优化 BIP47。以前,解决问题的主要思路是,在 BIP47 中,你需要使用自己的输入来发送消息给接收方,这样你就把自己的一些币跟接收者关联起来了:你有给 TA 支付的意图。如果你可以让别人来代替你发送通知,这个关联就被打破了。而且这样一来,你还可以把通知交易打包起来发布。只要有一个人同意帮助你,他们就可以把一条消息放在自己的交易的一个 OP_RETURN 输出中,或者使用一些技巧,把消息放在 witness 数据中。有一些技巧可以帮助你缩小消息的体积。发送者的公钥必须放在 witness 数据中,接收者必须知道这条消息对自己有意义。你可以只使用接收者公钥的前 4 个字节,这可能会遇上碰撞巧合,但事实证明,其中的成本并不高,接收者只需生成一个 xpub 然后开始监控其地址,并不是什么很高的代价,所以不必担心混淆。
BIP47 的一个取舍,或者说一个设计目标,是尽可能与现有的软件兼容。有些人希望让新技术接受起来尽可能简单;也有人希望让技术更加高效,但客观上会导致整个生态不得不增加工作量。BIP47 作出的选择是,尽可能兼容现有的软件:(作为支付方)你可以通过创建一个输出来通知你的收款方,这儿有一个只有你能花费的地址;(作为收款方)你也可以按当前可用的方式意识到一个新地址,并看到这个地址按照常见的支付流程得到支付。
这套协议的一个明显的缺点是,汇集这些通知的人必须在协议外得到支付,比如通过闪电网络。
BIP 47 的变种:隐私支付(BIP351)
还有另外一个变种,它基于布拉格协议,作出了一些微小的调整和一个重大的辩证。它用盲化替换了外包。所以,你不需要请求其他人把信息放到区块链上,你可以盲化信息,使得没人能看出来接收者是谁。虽然这是一个很棒的事情,但它也引入了另一个问题,就是接收者不知道哪一条消息对自己是有意义的。所以现在他们需要下载所有的通知交易,而在前面两种协议中,接收者只需要查看包含了其 xpub 的消息。现在他们需要检查所有的通知交易了。
从好的一方面看,不需要在协议外给打包员支付了。这也是一种取舍,我个人不知道这到底算不算一种优化。我觉得人们也会有不同的意见。一方面来说,你不再需要发起协议外的支付了,但现在,轻客户端要检查所有的消息,会更难处理。我个人并不认为这是一种优化,但它也是一件事。这个协议也使用 OP_RETURN 来放置信息。
支付者可以把消息放到 OP_RETURN 输出中并发送到链上。这肯定与当前的软件不兼容,所以需要某种办法,将 OP_RETURN 信息转发给需要使用这种协议的客户端。
Robin Linus 的隐身地址方案
跳过,我会在后面提供一个连接。请关注我的推特。放出这个视频的时候,我会贴一条关于这种方案的链接在下面。
静默支付
静默支付选择了另一种取舍。我们依赖于已经放在区块链上的数据来生成共享秘密值。给定你要给某人支付,你已经拥有了用来给 TA 支付的公钥 —— 你的输入的公钥。为什么不直接使用这个输入公钥,以及接收者公开的公钥呢?为什么不使用这两者推导出一个共享私钥?这是可以做到的。而且不需要额外的链上数据。实际交易看起来就跟别的交易一样,没有分别。很棒。
但现在压力推到了扫描这边:接收者不知道自己是否得到了支付。发现支付的唯一方法就是检查每一条交易并遍历它(译者注:指输入),推导出一个共享秘密值,看看这个地址是否属于自己。关于轻客户端如何支持这种协议,我已经有一些想法了。大体意思是说,如果你已经在运行一个全节点了,那么额外的工作是微不足道的。你已经投入了力气来验证所有区块了,现在你只需要为新接收到的每一笔交易多运行一次椭圆曲线操作。作为一个全节点,这是微不足道的。这样一来,你只需公开一个公钥,任何人都可以给你支付,而且你一定会知道给你的支付。
还有一些复杂性。因为时间关系,这里不再展开。
概述
如果你的情况是,你可以跟支付者至少交互一次,我的建议是,你可以直接给出一个 xpub 公钥,这样他们就能给你反复支付。如果服务端知道你的公钥,而且你的轻客户端会把公钥交给服务端,那么你应该使用免信任的地址服务端。如果你运行自己的服务端,你可以直接运行 btcpay 服务端或者你自己的免信任地址服务端。如果你接受链上通知的想法,你可以使用 BIP47 或者它的变种。如果你运行自己的全节点,那么我认为静默支付也是一种显然可以工作的模式。
我还制作了一些关于 coinjoin 的幻灯片。
现在 woxlt 在 github 上发布了一个静默支付的实现。这是一个概念验证项目,没有指望要合并到 Bitcoin Core 中。性能看起来不错。他作了一些更改,比如加入了多个输入。支付目的标签是一种方法,即使你只有一个地址,也能知道别人为什么给你支付。也许你需要接收捐赠,但这些捐赠有两种不同的目的;又或者,你需要区分它们。欢迎在静默支付的代码、代码审核和 BIP 撰写上的贡献。现在我个人有太多正在推进的项目了,但我很乐于监督和帮助你。
(完)