作者:Gloria Zhao
来源:https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-May/020493.html
我正在撰写一份提案,建议改变当前的点对点协议以启用 “交易包转发(package relay)” 特性,并征求对其中的设计和方法的反馈。这里是一个链接,汇总了最的提案:
https://github.com/bitcoin/bips/pull/1324
(译者注:该 PR 已经在 2022 年 12 月关闭;另一个跟踪交易包转发提议的地方在此处。)
如果你有概念或者方法上的评论,请在邮件组里回复,好让每个人都能审核并参与到讨论中来。如果你找出了拼写错误或不准确的用词,可以尽情在 PR 页面中留下你的建议。
本文剩下的部分将包含跟提案相同的内容,只不过经过了一些重新排序并提供了额外的上下文。如果你还没有完全跟上我们对交易包转发特性的讨论,或者发现提案本身难以理解,本文这种形式应该会更详细也更有说服力。
背景和动机
用户会创建和广播依赖于(即花费)未确认交易的交易。“包(package)” 是一个广泛使用的术语,指的是一组交易,可以用一个带连接的 “有向无环图(directed acyclic graph)” 来表示,其中,带箭头的边表明一笔交易在花费另一笔交易。
与矿工激励兼容的交易池策略(policies),可以帮助创建一个公平的、基于手续费的区块空间市场。因为矿工会为了赚取更高的区块奖励而尽可能多收交易手续费,若某些交易池策略可以让本地交易池内容跟矿工交易池趋同,则不挖矿但参与交易转发的用户也可以从这样的交易池策略中得到许多好处,包括更快的 “致密区块” 转发速度、更准确的手续费估计。此外,用户也可以利用交易池和兼容矿工激励的策略,来提高自己的交易的区块确认优先级,办法是为待确认的交易附加高手续费的后代交易(这叫 “子为父偿(CPFP)”。在控制交易池的入口时,一次只考虑一笔交易,会限制节点确认哪笔交易具有最高手续费率的能力,因为它无法考虑后代交易,除非所有交易都进入交易池。类似地,它也无法在遇到两笔相互冲突的交易(也即用户发起 “手续费替换(RBF)” 的情形)、考虑要保存哪一笔时使用交易的后代。
当某个用户的一笔交易够不上一个交易池的最低费率门槛、而他又无法直接创建一笔替换交易时,他的交易就会被这个交易池直接拒绝。他也无法附加一个后代交易,以击败另一笔冲突交易。这样的局限性,伤害了用户为自己的交易追加手续费的能力。此外,对依赖于预先签名的时间锁交易(以防止欺诈)的合约式协议 —— 比如,LN Penalty 中的 HTLC-Timeout 交易 1 2 3 、Revault 中的 Unvault Cancel 交易 4 、谨慎日志合约中的退款交易 5、eltoo 中的更新交易 6 —— 来说,它还意味着一个安全问题。换句话来说,许多合约式协议的一个关键的安全性假设,正是所有参与者都能及时地广播和确认自己的交易。
过去几年中,“钉死攻击” 已经吸引了越来越多的关注 0 1 2 3 6 ;在这种审查攻击中,攻击者利用了交易池策略的限制,来阻止一笔交易被转发和确认。简而言之:用于退出合约的交易必须得到一定数量的确认才能生效,但这些交易的手续费率在广播之前就已经确定了。如果提前设定的手续费率太低、又没有追加手续费的方法可用,攻击者就可以从合约对手那里偷盗资金。我在一个演讲上详细介绍了一个偷盗闪电通道 HTLC 输出的例子(就在视频的约 23:58 处 7 8)。注意,绝大部分攻击都仅在广播时的区块空间市场要求比签名时高得多的手续费率时才能奏效。总是高估手续费也许能暂时回避这个问题(如果交易池流量较低而且可以预测的话),但这种解决方案并不是完美无缺的,而且会浪费用户的资金。手续费率市场可能会因为流量激增(比如几天前达到了 12 聪/vB )或者持续的高支付量(比如 2021 年 4 月和 2017 年 12 月的情形)而改变。
最好的解决方案是,让节点能够将交易包作为一个单元来考虑,例如,将一笔低手续费的父交易和一笔高手续费的子交易一起考虑,而不是分开考虑。一个能够感知交易包的交易池策略,可以帮助确定拒绝一笔没有满足手续费要求的交易是否真的明智。并且,整个网络大范围采用这样的策略,将为区块空间创建一个更纯粹基于手续费率的市场,并让合约式协议能够在交易广播时调整手续费(即其挖矿优先级)。交易包转发特性所需的一些支持已经在 Bitcoin Core 中存在了。从 0.13 版本开始,Bitcoin Core 就使用 “祖先交易包(ancestor packakges)” 而不是交易单体,来评估交易池内交易的激励兼容性 10 ,并据此将这些交易打包入块 11 。
交易包转发,其概念是在点对点网络上的节点之间传播的 {annoucing, requesting, downloading} 包,也已经讨论了许多年了。我可以找到的最早的公开讨论是在 2015 年 12 。交易包转发特性的两个最常见的应用场景是:为手续费过低的交易追加手续费,以及,减少孤儿交易的数量。假设每个人都在原则上想要交易包转发特性,应该是没有异议的 —— 只是各人的紧急性评价不同。过去几年中,其他人已经做了大量的工作,我就受益于 13 14 15 16 。
我的方法是将项目分成两个部分:(1)交易池的交易包把关(Package Mempool Accetp),包含了验证逻辑和交易池策略;(2)交易包转发,包含了对 P2P 协议的变更。
项目进度:
在跟多位合约式协议(主要是闪电网络)的开发者讨论之后,我们确定,交易包里面只要包含一个子交易,及其所有未确认的祖先交易(比如,子交易 + 未确认的父交易,或者,一笔子交易 + 多笔未确认的祖先交易),对他们来说就已经够用了 —— 需要的只是为预先签名的交易追加手续费。一个 子交易+未确认的父交易 的交易包有许多特性,让许多东西更容易分析。
几个月前,我提出了一组策略,用于对这种受限制的拓扑图所表示的交易包运行安全验证和手续费估计 17 。一系列实现这个提议的 PR 已经合并到了 Bitcoin Core 中 18 。
理论上来说,开发一种安全且激励兼容的交易包把关策略,就足以解决这个问题。节点可以适时地接受交易包(例如,通过尝试从交易池中拒绝的交易的组合),但这种实践很可能是低效的,甚至会开启新的拒绝式服务攻击界面。额外的 P2P 消息也许可以让节点以更高效的通信方式请求和分享跟交易包验证相关的信息。
给定只有 “交易包 RBF” 属于交易包把关策略,而且我们可以并行推进 P2P 协议和交易池策略的开发,我认为,可以提出一种交易包转发提案。
提案
本提案包含两个部分:一种 “通用” 的交易包转发协议,以及,它的一个插件,“子交易 + 未确认的父交易” 交易包,作为版本 1 的交易包。可以创建出另一种版本的交易包,“具有未确认祖先的交易” ,以延伸交易包转发的范围、消除孤儿交易。
通用的交易包转发特性
我们的两种主要的想法是:
- 在下载交易包的同时验证它。
- 提供信息,以帮助对等节点确定是否要请求某个交易包的一部分的一笔交易,以及/或者 如何验证它。
预期协议流程
由于分布式交易转发网络的匿名特性,节点可以不会一次性受到验证一笔交易所需的所有信息。举例来说,在一个节点完成 “初始化区块同步(IBD)” 之后、刚开始参与交易转发的时候(此时其交易池是空的),受到孤儿交易是很常见的。在这种情况下,这个节点知道自己错过了一些信息,一个 “接收者发起” 的对话是合适的:
- 接收者请求交易包的信息。
- 发送者提供交易包的信息,包括交易包中的交易的 wtxid 以及其它可能有用的信息(例如,交易包的总交易费和体积)。
- 接收者使用交易包的信息来决定如何请求及验证交易。
有时候,不论一个节点以什么样的顺序收到交易,单独验证它们都是不够的。当发送者感知到接收者需要更多信息来接受一个交易包的时候,应该允许主动的 “发送者发起” 的对话:
- 发送者宣布自己拥有跟某一笔交易有关的交易包信息,如果没有这些信息,该交易可能会被认为是无价值的。
- 接收者请求交易包信息。
- 发送者提供交易包的信息,包括交易包中的交易的 wtxid 以及其它可能有用的信息(例如,交易包的总交易费和体积)。
- 接收者使用交易包信息来决定如何请求及验证交易。
交易包转发在两个节点的 “版本握手(version handshake)” 期间协商。交易包转发要求两个节点都支持基于 wtxid 的转发,因为包内的交易是通过它们的 wtxid 来引用的。
新的消息类型
我们要添加三种新的协议消息类型 —— 在交易包转发特性的任何版本中都需要。此外,每一种版本的交易包转发必须定义自己的 inv
类型和 “pckginfo” 消息版本,在本文档中分别称为 “MSG_PCKG” 和 “pckginfo”。更具体的例子见 BIP-v1-package 。
- sendpackages
字段名称 | 类型 | 体积 | 目的 |
---|---|---|---|
version | unit32_t | 4 | 指定该节点所支持的交易包版本 |
max_count | unit32_t | 4 | 指定该节点愿意接受的交易包的包内交易数量上限 |
max_weight | unit32_t | 4 | 指定该节点愿意接受的交易包的总重量上限 |
- 这种 “sendpackages” 的消息具有上述结构,并且 pchComand == “sendpackages”
- 在版本握手期间,节点应该发送一条 “sendpackages” 消息,以表明自己支持交易包转发,而且可能请求交易包。
- 这种消息应该包含一个该节点支持的版本的版本好。节点应该为自己所支持的每一个版本发送一条 “sendpackages” 消息。
- 这种 “sendpackages” 消息必须在发送 “verack” 消息之前发送。如果在收到 “verack” 之后才收到 “sendpackages” 消息,接收者应该断开跟发送者的连接。
- 如果一个节点的版本信息中包含了
fRelay==false
,那就不应该发送 “sendpackages” 给他们。如果一个节点已经在自己的版本消息中包含了fRelay==false
,但依然收到了 “sendpackages” 消息,那应该断开跟消息发送者的连接。 - 在收到一条带有己方不支持的交易包版本的 “sendpackages” 消息时,节点应该当自己从未收到过这条消息。
- 如果对等连接的双方都发送了带有相同版本号的 “wtxidrelay” 和 “sendpackages” 消息,那么双方可以宣布、请求和发送交易包信息给对方。
- getpckgtxns
字段名称 | 类型 | 体积 | 目的 |
---|---|---|---|
txns_length | CompactSize | 1 字节或者 3 字节 | 所请求的交易的数量 |
txns | wtxid 的列表 | tens_length * 32 | 交易包内的每一笔交易的 wtxid |
- 这种 “getpackgtxns” 消息具有上述结构,而且 pchComand == “getpackgtxns” 。
- 一条 “getpackgtxns” 消息可以用来请求之前在一个 “pckginfo” 消息中宣布的全部或部分交易,交易以 wtxid 来指定。
- 在收到一条 “getpackgtxns” 消息之后,节点要么响应以包含了所请求的交易的 “pckgtxns” 消息,要么响应以一笔 “notfound” 交易,表示缺失其中一笔或多笔交易。这使得接收者可以避免下载和存储无法立即得到验证的交易。
- “getpackgtxns” 消息只应该在对等连接双方在版本握手中已经同意发送交易包之后发送。如果在谈判好交易包转发之前收到一条 “getpackgtxns” 消息,接收者应该断开连接。
- pckgtxns
字段名称 | 类型 | 体积 | 目的 |
---|---|---|---|
txns_length | CompactSize | 1 字节或者 3 字节 | 所提供的交易的数量 |
txns | 交易的列表 | 可变长度 | 交易包内的交易 |
- 这种 “pckgtxns” 消息具有上述桔梗,而且 pchCommand == “pckgtxns”。
- “pckgtxns” 消息应该包含被 “getpckgtxns” 消息请求的交易的数据。
- “pckgtxns” 消息只应该发送给使用 “getpckgtxns” 消息请求了交易包的对等节点。如果收到了一个主动提供的交易包,应该断开跟发送者的连接。
- “pckgtxns”消息只应该在对等连接双方在版本握手中已经同意发送交易包之后发送。如果在谈判好交易包转发之前收到一条 “packgtxns” 消息,接收者应该断开连接。
版本 1 交易包:子交易 + 未确认的父交易
这个插件通过定义版本号为 1 的交易包、 一种 “pckginfo1” 消息以及一种 MSG_PCKG1
inv 类型,延伸了交易包转发的特性,使得交易包能包含一笔交易及其所有未确认的父交易。它使得子交易可以为其手续费太低的父交易(以及他们在交易池中的竞争交易)支付手续费。
(译者注:关于版本 1 的交易包,原文前后一致地使用 “chaild-with-unconfirmed-parents”。由于一笔比特币交易可能使用来自多笔交易的输出,所以使用复数形式的 “parents” 并不奇怪。但作者并没有明说这里只能包含两代交易。也就是说,这里的 “parents” 应该当成 “祖先”/前序交易 来理解,而不是只能包含子交易的直系前代交易。)
预期协议流程
在转发一个由低手续费的父交易(们)和高手续费的子交易组成的交易包时,发送者和接收者按下列步骤操作:
- 发送者使用 “inv(MSG_PCKG1)”宣布自己拥有一个 子交易 + 其未确认的父交易 的交易包,该子交易为其手续费过低的父交易(们)追加手续费。
- 接收者使用 “getdata(MSG_PCKG1)” 请求交易包信息。
- 发送者使用 “pckginfo1” 提供交易包信息,包括发送者已知的最新区块的区块哈希值、包内交易的 wtxid,整个交易包的总手续费和总重量。
- 接收者使用这些交易包信息来决定如何请求交易。例如,如果接收者的交易池中已经有了部分交易,他们就只需请求缺失的那些交易。他们也可以决定完全不请求任何交易。
- 收到一条 “pckgtxns” 消息之后,接收者提交作为一个交易包的交易。
新的信息类型
添加一种新的 inv 类型 “MSG_PCKG1” 以及新的协议消息 “PCKGINFO1”。
- pckginfo1
字段名称 | 类型 | 体积 | 目的 |
---|---|---|---|
blockhash | unit256 | 32 | 定义该交易包的最新区块(chain tip) |
pckg_fee | Camount | 4 | 交易包内的所有交易所支付的总手续费 |
pckg_weight | int64_t | 8 | 交易包内的所有交易的总重量 |
txns_length | CompactSize | 1 或 3 字节 | 所提供的交易的数量 |
txns | wtxid 的列表 | txns_length * 32 | 包内的每一笔交易的 wtxid |
- 这种 “pckginfo1” 消息具有上述结构,并且 pchCommand == “pckginfo1”。
- 一条 “pckginfo1”消息包含了一个版本 1 交易包的信息,由包内交易的 wtxid 和当前的区块哈希值指定。
- 在收到一条 “pckginfo1”消息的时候,节点应该决定是否要验证这个交易包、请求必要的交易数据,等等。
- 在收到一条错误格式的 “pckginfo1” 消息或者包 —— 比如,不遵守
max_count
、max_weight
或由双方在初始化协商中一致同意的版本所定义的规则 —— 时,接收者应该断开连接。如果一个接收者收到了一条 “pckginfo1” 消息,其 “pckg_fee” 或 “pckg_weight” 并没有反映真实的总费用和相应的总重量、包内交易时,该消息也是格式错误的。 - 节点不得在接收者未请求时发送 “pckginfo1” 消息。在收到一条不请自来的 “pckginfo1” 消息时,节点应该断开连接。
- 仅在双方在版本握手节点都同意发送版本 1 的交易包时,才能发送 “pckginfo1” 消息。如果节点收到了一条来自并未协商好交易包转发规则的对等节点的 “pckginfo1” 消息,应该断开跟发送者的连接。
- MSG_PCKG1
- 加入一种新的 inv 类型(MSG_PCKG1 == 0x6),用于跟版本 1 的交易包有关的 inv 消息和 getdata 请求。
- 作为一种 inv 类型,它指定了交易的数据以及对该交易有用的版本 1 交易包信息。交易以其 wtxid 指定。作为一种 getdata 请求类型,它表明发送者想要该交易的交易包信息。
- 收到一个 “MSG_PCKG1” 的 “getdata” 请求之后,节点应该响应以对应于所请求的交易及其最新区块的版本 1 交易包,或者是
NOTFOUND
。节点不应假设交易方同时也在请求交易数据。
子交易 + 未确认父交易 的交易包规则
转发一个 子交易 + 未确认父交易 的交易包的两个节点,必须遵守如下规则,否则交易包就是错误格式的,接收者应该断开跟发送者的连接。
一个版本 1 或者 “子交易 + 未确认父交易” 的交易包,可以为任意一笔花费了未确认的输入的交易定义出来。这笔子交易可以认为是这个交易包的 “代表”。这个交易包可以被该交易的 wtxid 以及当前链的最新区块的哈希值唯一标识。
一个 “子交易 + 未确认父交易” 交易包必须是这样的:
- “经过拓扑排序的(sorted topologically)”。对于交易包中的每一笔交易 t,只要 t 的任何父交易还在交易包中,这笔父交易都必须出现在列表中先于 t 的位置上。换句话说,包内交易必须按照祖先交易数量的升序来排序。
- “只有一笔带有未确认父交易的子交易”。交易包由一笔交易及其未确认的父交易组成。交易包中不能有任何别的交易。交易包内可能有其它依赖关系,可以从拓扑顺序中表现出来(例如,一笔父交易可能花费了另一笔父交易的输出)。
- “所有未确认的父交易”。子交易的所有未确认的父交易都必须出现。
- “没有冲突”。交易包内的交易不能相互冲突(即,花费相同的前序输出)。
- “总手续费和总重量”。
total_fee
和total_weight
字段必须准确地分别表示表示包内所有交易的手续费和重量(由 BIP141 定义)。
并非该子交易的所有父交易都必须出现;这笔子交易可能也花费了已经确认的输入。但是,如果该子交易有已经确认的父交易,这些父交易不能出现在交易包中。
虽然一个 “子交易 + 未确认父交易” 的交易包在子交易具备相对最高的手续费率时最有用,但这个属性并不是构成一个有效交易包的必要条件。
梳理
问:发送者应该在什么情况下宣布一个 子交易 + 未确认父交易 的交易包?
应该在子交易达到了对等节点交易池的手续费门槛、但其一笔或多笔父交易未能达到门槛的时候宣布;应该为这笔子交易发送一条 “inv(MSG_PCKG1)” 消息,而不是 “inv(WTX)” 消息。每一笔沟道对等节点交易的手续费门槛的父交易都仍然应该正常宣布。
问:如果在消息传送期间,一个新的区块到底了,那该怎么办?
子交易 + 未确认父交易 的交易包 是通过一笔基于最新的链状态的交易来定义的。因此,如果一个新的区块到达,它可能会减少交易包内的交易的数量(即,包内的某一些父交易可能会得到确认)。在链重组中,交易包内的交易的数量既可能减少,也可能增加(即,子交易的某一些父交易在某个区块中得到确认,但该区块后来被击败)。
如果一个新区块在 “getdata” 或者 “pckginfo1” 消息之前达到,那就不需要改变什么。
如果一个新区块在 “getpckgtxns” 或者 “pckgtxns” 消息之前到达,接收者可能需要重新请求交易包信息 —— 如果新到达的区块包含了交易包内的交易。如果区块并不包含交易包内的任何交易,则无论它是否延续了前一个最新区块或导致了链重组,都不需要改变流程。
问:“getpckgtxns” 和 “pckgtxns” 消息可以只包含一笔交易吗?
可以。
未来的协议拓展
要引入一种的新的交易包,只需为之安排一个版本号 “n”,并在节点的版本握手期间使用额外的 “sendpackages” 消息沟通对它的支持。同时,应该为新的交易包类型定义额外的交易包信息消息 “pckginfon” 以及 inv 类型 “MSG_PCKGn”。不过,“getpckgtxns” 和 “pckgtxns” 不需要改变。
一种 “带有未确认祖先的交易” 包案例提议可见:19
兼容性
更老的客户端在这一变更之后保持完全的兼容性和互通性。实现了这套协议的客户端只会尝试跟版本握手期间达成一致的对等节点发送和请求交易包。
交易包 Erlay
使用 BIP330 基于协调(reconciliation)的交易转发提议(Erlay)的客户端可以使用交易包转发,不受干扰。事实上,一个交易包既可以用 Erlay 来宣布,也可以通过交易包转发特性来宣布。在协调之后,如果发起者已经宣布了一笔交易的 wtxid,但也拥有该交易的交易包信息,他可以发送 “inv(MSG_PCKG)” 而不是 “inv(WTX)”。
设计哲学
P2P 消息的设计
这些 P2P 消息是为了通信效率而添加的,而且,因此,应该基于沟通(不一定值得信任的)信息所花费的资源来度量替代方案:我们希望尽可能降低网络带宽开销、避免下载同一笔交易两次及以上、避免下载最终会被交易池驱逐的交易,以及,尽可能减少分配给尚未验证过的交易的存储空间。
考虑交易转发中的这些(模棱两可)的情形:
Alice(发送者)正在给 Bob(接收者)转发交易。Alice 的交易池的手续费门槛是 1sat/vB,而 Bob 的手续费门槛是 3sat/vB 。为了简洁,假设所有交易都是 1600 Wu 的虚拟体积、都是 500 字节的序列化体积。除了被指定的花费关系之外,所有其它输入都已经得到了确认。
- 交易包 {A, B} ,其中 A 支付了 0 聪手续费,而 B 支付了 8000 聪手续费。
- 交易包 {C, D} ,其中 C 支付了 0 聪手续费,而 D 支付了 1200 聪手续费。
- 交易包 {E, F, G, H, J} ,分别支付了 4000、8000、0、2000 和 4000 聪的手续费。
考虑过的其它设计
“仅转发交易包信息”:仅仅设置 “pckginfo” 消息,给出足以让接收者接受交易包的信息。省去 “getpckgtxns” 和 “pckgtxns” 。虽然这种设计在因为某些原因无法批量下载区块时是一个好的后备方案,但不应默认使用它,因为它 总是 给未经验证的交易分配存储空间。
“不设交易包信息沟通轮次”:取消沟通交易包信息的轮次,直接使用子交易的 wtxid 指代整个交易包,并总是一起发送整个交易包。这将导致节点重复下载交易。
我也创建了一个幻灯片,探讨了多种替代性的设计,以及它们会失败的一些案例 20 。请尽情提出其它方案。
版本系统
这套协议可以延伸到支持多种类型的交易包,只取决于未来所需的应用场景。我们考虑了两种版本控制的 “口味”:
- 当交易池的交易包把关升级到支持更多类型的交易包时,就递增版本号(类似于 Erlay)。在版本握手期间,对等节点通过每人发送一条 “sendpackages” 消息来协商交易包转发的版本。
- 每次引入另一种交易包类型,就为之分配一个版本号,并宣布它作为一种额外受支持的版本(类似于 “致密区块转发”)。在版本握手期间,对等节点为自己支持的每一个版本发送一条 “sendpackages”消息。
我们偏爱第二种,因为它允许为不同的版本设计不同的参数。举个例子,它可以既支持 “任意拓扑图,但最多只能有 3 笔交易”,又支持 “子交易 + 未确认的父交易,使用默认交易池祖先数量限制”。
致谢
我希望清楚地表明,“交易包转发” 的概念不是本提案发明的,事实上,本提案建立在许多人经年累月的工作上,包括 Suhas Daftuar 和 Antoine Riard。
感谢 John Newbery 和 Martin Zumsande 在设计上的贡献。
感谢 Matt Corallo、Christian Decker、David Harding、Antoine Poinsot、Antoine Riard、Gregory Sanders、Chris Stewart、Bastien Teinturier 和其他人在合约式协议所需的接口上的贡献。
期待听到你的想法!