作者:Mark Erhardt

来源:https://btctranscripts.com/tabconf/2022/2022-10-15-segwit-vbytes-misconceptions/

本文为作者在 TABcof 2022 上的演讲文字稿。记录者为 Bryan Bishop。

度量交易的重量:见证数据的折扣

你应该已经从别人那里听到了,我的演讲会有更多个互动。我可能不会讲完我准备的所有内容。如果你在听的过程中有所疑惑,欢迎举手,这样我们就可以立即解决问题。我准备带各位了解一下非隔离见证交易和隔离见证交易的序列化。希望演讲结束之后,你可以理解交易的重量(weight) 是如何计算的,以及见证数据的折扣是如何适用的。最后,我们应该会看看一些不同的输出类型。

隔离见证以前

在隔离见证激活之前,一笔交易是这样的:它将至少有一个输入和一个输出。这笔交易有一个 P2PKH 输入,但有 4 个输出,一个是封装的隔离见证输入,一个是传统输入。到目前为止,没什么大不了的。估计你也看过这种类型的交易。

交易的序列化

打开引擎盖,我们来看得仔细些。如果你序列化一笔交易,或者观看一笔交易的序列化形式,就像 yogh.io 给不同数据染色一样,你可以看到,所有的数据都编码成十六进制的字符串,而且这些字符串是使用不同的函数来处理的。

我们首先会看到交易的元数据。每一笔交易都有一个 “交易头”,包含一个 4 字节的版本字段字段、输入容器(告诉你这笔交易有多少个输入)和输出容器(告诉你这笔交易有多少个输出),以及一个 4 字节的时间锁字段。

如果有人关注了最近关于 v3 交易的进展,这就是交易版本字段。如你所见,1 在最前面,因为这个值是小端序的(little-endian),不是大端序的(big-endian)。

看完交易头之后我们来看看输入。

输入的序列化

若我们要使用一个输入,我们首先必须告诉大家我们要花费的是哪个 UTXO。为了唯一地定位一个 UTXO,我们使用了 outpoint(“输出点”),就是一个交易索引号(txid)以及该 UTXO 在这个交易的输出列表中的位置。在我的幻灯片中,txid 是深蓝色的。这是这个 UTXO 的来源交易。作为计算机科学家,我们从 0 开始计数。

要花费一个 UTXO,我们必须满足编码在这个 UTXO 中的条件脚本。这是用 “输入脚本” 来作的。输入脚本的长度是任意的,取决于我们想要满足的脚本的条件类型。因此,我们必须将输入脚本的长度编码(“输入脚本长度”)进输入。

在这个数值之后就是输入脚本本身。以 P2PKH 输入为例,这是一种众所周知的脚本。P2PKH 条件脚本意味着资金被锁定在一个公钥的哈希值里。为了满足 P2PKH 脚本,我们要提供这个公钥,然后运行哈希计算,证明这个哈希值与存储在条件脚本里的一致,然后再提供该公钥的一个签名。

输入的最后一个字段是 “sequence”。使用这个字段,我们可以指定这笔交易是否可以被替换。一笔交易只要有一个输入的 sequence 值小于可能的最大值,这笔交易就不算是最终版本,是可以替换的。只需要在一个输入中指定,就足以让一笔交易变成可替换的。要是这个字段的值小于 最大值 - 1 呢? 那就意味着它启用了时间锁。我在此不赘述了。

输出的序列化

这个示例交易有 4 个输出。我要拿出其中一个来细说。输出里的第一个字段是 “面额”,也就是进入这个锁定资金的条件脚本的聪的数量。一个输出会创建一个新的 UTXO,而一个 UTXO 会有确切的面额。然后,UTXO 还有一个锁定脚本,也就是条件脚本,指明可以花费这个 UTXO 的条件。

Q:你能在一笔交易中分配的最大数额是多少?

A:面额字段有 8 个字节长,所以我觉得它足以放下 2100 万 BTC。

Q:为什么要使用整数呢?

A:有些人可能已经知道了,但我们在协议开发中从不使用浮点数。比特币协议的原生单位就是 “聪”。我们在这里分配的是整数个聪。我们不以 BTC 为单位,是因为浮点数很糟糕,可能导致我们不希望在金融协议中出现的舍入误差。

所以,我们使用 8 个字节的字段来编码面额。这也是小端序的。我们有输出脚本,先是输出脚本的长度,然后是输出脚本本身。这是一个 P2SH(支付给脚本哈希值)脚本。脚本中的哈希值是 20 字节长的,是一个 hash160 哈希值。

原生的隔离见证

我应该先说明一下什么是原生的隔离见证。许多人把第一种类型的原生隔离见证输出称为 “原生的隔离见证输出”。你可能很熟悉 P2WPKH 输出,这种地址以 “pc1q” 开头。他们是 0 版本的原生隔离见证输出。差不多一年前,2021 年 11 月,我们有了另一场软分叉,引入了另一种输出类型,叫做 “支付给 taproot(P2TR)”,它也是一种原生的隔离见证输出牧场原生隔离见证 v1 输出,其地址以 “bc1p” 开头。“p” 在 bech32 编码种表示 1。我的这个案例是 P2TR 输出。

Q:那封装的隔离见证输出呢?它算是另一种输出类型码?

A:封装的隔离见证输出有点像混合物,因为封装的隔离见证输出是为了通过满足 P2SH 的规则来实现向后兼容,但它的见证数据部分跟原生隔离见证输出是完全相同的。在封装隔离见证交易的见证数据中,你会用到跟原生的隔离见证交易完全相同的矫正数据。后面我们会讲得再详细一些。

在封装的隔离见证输出和原生的隔离见证输出之间,区别只在于(前者有)一种中介脚本(fowarding script)说你去看看见证数据字段吧,这就是我们满足花费条件的方式。(……)我们也没有为原生的 taproot 输出实现嵌套的隔离见证输出,所以只有 v0 的隔离见证输出有封装的形式。现在,人们可以分辨原生的隔离见证输出了。

原生的隔离见证 v1

这是比特币区块链上被花费的第一笔 P2TR 交易。它还附带了一条消息 “我喜欢 Schnorr 签名,我不能说谎”。这是一个 bc1p 地址,表明这是一个 P2TR 输出,而且它把找零也分配给了一个 P2TR 输出。我们来看看它的序列化形式。

看起来跟我们上面的例子很相似,只是有一大片橙色在这里。这里所有的橙色标记的都是见证数据。其它方面也有一些区别。例如,这笔交易的输入脚本的长度为 0,因为在原生的隔离见证输入种,根本就没有输入脚本,我们把满足花费条件的数据放到了见证数据种。所有的原生隔离见证输入都可以一眼认出来,因为它们的输入脚本长度为 0。

再来看看见证数据。谁还记得,在非隔离见证交易中,版本号后面应该接什么数据?没错,输入容器,要记录输入的数量。在一开始,我说过,一笔交易至少要有一个输入和一个输出。如果一个节点不理解原生隔离见证输出的格式,然后发现一笔交易的输入数量是 0,他会怎么做?他会认为这笔交易是无效的,会丢掉这笔交易,并禁言给他这些垃圾的对等节点。我们在这里放了一个标签,意思是说,嘿,这是一种新的格式,老节点们不应该读取这里面的东西,你得到它只是意外,不必花力气解析了。然后,这个标签暗示这笔交易有一个见证数据(……)。

Q:你这里说的 “无效” 是什么意思?交易池不是有一个标准的吗?有一些交易虽然不符合标准,但依然是有效的?还是说这是一种转发策略呢?

A: …… 这笔交易的这种表现形式似乎是无效的,因为网络中已经基本不存在非隔离见证的节点了,最后一个非隔离见证的 bitcoin core 版本是 0.12,已经是 10 个版本以前了,大概是在 5 年前发布的,而且我们维护一个版本的生命周期一般是两年。所以,解释非隔离见证节点如何处理隔离见证交易基本上是一个纯理论问题,不再是一个有意义的重大问题了。我会用两张幻灯片来回答你的问题。无效的意思是,如果我们把一笔交易的这种表现形式发送给一个非隔离见证节点,这个节点会认为这是一堆垃圾。

Q:这种情况很独特也很奇怪,但隔离见证刚好就是这种情况,我理解的是,老的隔离见证节点会跟非隔离见证节点沟通,他会建构一种不同的交易向量和区块。所以不会有这种时候。在这笔交易和一个有效的非隔离见证交易之间有一种同构。所以说它是一种非常令人困惑的 “非无效交易”。

A:这笔交易有另一种表现形式,是非隔离见证节点可以解析的。他们不需要见证数据,因为软分叉就是这样工作的,他们也完全无法理解见证数据。

所以,它暗示了这是一个隔离见证输出。为什么我们不统计见证数据堆栈的数量?因为在一笔隔离见证交易中,每一个输入都必须拥有一个见证数据堆栈。在这笔交易中,我们只有一个输入,所以我们会有一个见证数据堆栈来满足这个输入。所以我们首先有见证数据的数量,然后是见证数据的长度,在这里就是第一个也是唯一一段见证数据的长度。

隔离见证的区块空间

在隔离见证以前,区块体积的上限是 1 MB。我们只需统计一个区块里的所有数据的原始字节数,最多只能有 100 万字节,超过了就说明这是一个无效区块。但在隔离见证之后,我们为区块空间引入一种 “重量” 限制,是 400 万个重量单位。因为我们只会把非见证数据传递给旧节点,所以他们只能看到这些绿色的数据,并且这些数据总是小于 1 MB。但是对于理解隔离见证规则的节点,我们会给他们见证数据,所以总的数据量是超过 1 MB 的。在 8 月份,我们看到一个创造了新高的区块,大小是 2.77 MB。

隔离见证节点会看到完整的交易。一种常见的误解是:见证数据不是交易的一部分、不是区块的一部分。但是,隔离见证节点可以看到完整的隔离见证交易。对于不理解隔离见证规则的节点,交给他的数据就是不包含见证数据的交易,看起来跟传统输出的交易非常相似,也会有版本号、输入容器、输出点(告知这笔交易花费的是哪一个 UTXO,这样节点就可以从 UTXO 集合中删去它)、输入脚本(只不过它的长度恰好是 0),然后是 sequence 字段,然后是输出的脚本和锁定时间。所有这些部分,对非隔离见证的节点来说,跟隔离见证以前的交易都是一样的,但非隔离见证的节点的安全性会较差,因为他们无法真正验证这些交易的签名。我们去掉了见证数据的标记和实体,仅仅把剥离后的交易交给了这些旧节点。这些部分比完整的交易要小,所以旧节点可以继续执行 1MB 的区块体积限制,但他们的安全性会差一些,因为这些节点无法获得输入脚本。

txid

如何在同一笔交易在隔离见证和非隔离见证节点中拥有同一个 txid?隔离见证引入的另一个变化是,在计算一笔隔离见证交易的 txid 时,将仅使用剥离后的交易部分。我们使用剥离后的交易来计算 txid,也就是这些数据的哈希值。这个变化也允许我们实现像闪电网络这样的协议,因为在闪电网络中,我们需要在一个输出出现之前构造花费这个输出的 “退款交易”。你不想把完整的、签好名的充值交易交给你的通道对手,因为这样他们就可以确定,资金已经去到了输出中,即时未来他们不再同意后续发生的交易,资金也确定无疑地进入了双方共有的输出中。我们必须在资金锁进双方共有的输出、产生风险之前,先同意后续的交易,比如退款交易,

所以,同一笔交易在隔离见证节点和非隔离见证节点中拥有相同的 txid,即使它在两种节点中的表现形式不同。

还有一种 “witness txid”,缩写为 “wtxid”。为了让索引号承诺隔离见证节点看到的完整交易,我们需要一种对完整的交易的承诺。(……)我们使用剥离后的交易,是为了获得 txid,这样我们就能在区块的默克尔根中承诺它,并以此为基础构建子交易。这就是我们修复交易熔融性(malleability)的方法。但是,对隔离见证节点来说,为了确保他真的获得了完整的交易,我们还需要在 coinbase 中放置一个见证数据承诺,它承诺了从完整的交易中计算出来的 wtxid 数值。

Q:如果一笔交易有多个输入,见证数据中是否有东西可以标记一段见证数据的结束点和另一段见证数据的起始点?

A:没有这样的东西。那么它怎么工作呢?我们前面有一个输入计数器,它会告诉我们有多少个见证数据堆栈。每个输入都有一个见证数据堆栈。传统类型的输入的见证数据的长度为 0。在我们的示例交易中,第二个见证数据堆栈将使用 P2TR。我们不必说明有多少见证数据堆栈。

交易的重量

要获得交易的重量,我们只需将非见证数据的字节数乘以 4,见证数据的字节数乘以 1,相加即可得出交易的重量。(……)选择这些系数纯粹是因为它们看起来不错。

那没有见证数据的交易又如何呢?交易有非见证数据的部分,我们会将其字节数乘以 4,作为其重量;以前我们的区块空间限制是 100 万字节,现在是 400 万重量单位。如果我们将所有非见证数据的 1 字节统计为 4 重量单位,则两种尺度是相同的:数据的大小变成了 4 倍,但空间的限制也变成了 4 倍。对旧节点来说,非隔离见证交易始终是一样的,因为他们看不见见证数据。(因为见证数据的存在)400 万重量单位是大于 100 万字节的。无论我们有多少隔离见证交易,剥离后的部分必须塞进一个小于 1 MB 的区块中。

Q:我听说有人提议取消见证数据的折扣。你能详细讲讲吗?

A:有一些人正在猜测在长期中取消见证数据折扣的方案能否软分叉到协议中。我的观点是,如果你把见证数据跟非见证数据同等看待,那么,交易的重量会严格变大 —— 它用到的区块空间会比当前规则下更小,所以我们应该是能够软分叉的。如果我们拥有跨输入的签名聚合技术(多个输入的交易只需使用一个签名),那么这样会使得签名聚合技术变得更有吸引力,因为它会从交易中移除更多成本,同时减少需要用到的区块空间;有些人认为这很棒,因为看起来我们的区块空间已经太大了。我不赞成这种想法,但我认为这是一种值得思考的理论情景。这样的提议可能是鼓励采用签名聚合技术的好方法。

Q:传统交易中的签名不会得到折扣,是吗?

A:传统的签名会统计成非见证数据。因为它们是放在输入脚本中的。非隔离见证的交易必须在输入脚本中提供见证数据 —— 姑且称为 “脚本陈述” —— 以满足条件脚本。这里是一个 P2PKH 输入,它在输入脚本中提供了一个公钥和一个签名,输入脚本会复制这个公钥、计算其哈希值、检查这个哈希值与其所花费的 UTXO 中的哈希值是否匹配,然后检查这个签名是否属于这个公钥。公钥和签名都在输入脚本中,因此都不会被统计为见证数据,因此其字节数乘以 4 是其重量。

Q:为什么你不谈谈见证数据折扣带来的最重要的特性呢?它产生了一种创建更大体积的交易的激励,因为这些交易的手续费反而更低。我以为你会讨论一种普遍存在的误解:这些交易更小或者更容易编码。

A:行,我们下一张幻灯片就会讨论这个。

不同的输出类型

我现在将列举所有可能的输出类型。

隔离见证软分叉扩大了区块空间,因为它允许一个区块包含更多的数据。但它同时也,一定程度上来说,并没有让交易的体积变得更小,至少对封装的隔离见证交易来说是这样的。P2SH-P2WPKH 脚本是封装的隔离见证单签名脚本。只有脚本陈述被放在见证数据部分,得到了折扣。在本页幻灯列举的 4 种输出中,P2TR 的体积最大。单从字节长度来看,一笔带有两个 P2PKH 输入和两个 P2PKH 的传统交易,体积跟单输入单输出的原生隔离见证 v0 交易基本上是一样。原生隔离见证交易并不显然更小。但是,因为它有折扣,原生隔离见证输入会消耗 68 vbyte,而 P2PKH 输入是 148 vbyte,差不多是两倍。实际上,P2TR 交易的字节数最小,全部加在一起也只有 312 字节,它比原生隔离见证交易和 P2PKH 交易要小大约 20%。所以,我部分同意旧的隔离见证类型在带宽上不算优化,但 P2TR 交易确实更小了。

(完)