Hacker Dōjo Workshop
资助金额:120 USDT
分享者:北邮在读硕士 Syshems
本项目由Hacker Dōjo资助,文章转载请联系
Telegram: @DoraDojo0
WeChat: @HackerDojo0
比特币的可扩展性问题是其面临的主要问题之一,也是许多人努力的方向,比如说闪电网络。但是,要在比特币网络中实现闪电网络,条件似乎还不具备,因为比特币自身编程模型存在一些缺陷。另一个解决方案 “隔离见证(Segregated Witness)” 也致力于提高可扩展性,但它同时也解决了许多问题,包括闪电网络实现所需修补的一些缺陷。本文中我们会讲解隔离见证的优势及其工作原理。
一、交易延展性/可塑性
隔离见证(SegWit)是一个由多个 BIP(141、142、143、144 和 145)描述的软分叉,**其主要用意是优化比特币交易和区块的结构,将交易的签名从交易中移到一个独立的结构中。**它不仅允许降低比特币交易的数据量大小(因此能让一个区块塞下更多的交易),也能解决 “交易延展性(transaction malleability)” 问题(也就是我们开头提到阻碍闪电网络实现的缺陷),对支付通道和闪电网络这样基于比特币交易结构的技术来说极为关键。
1、交易延展性
在现实生活中,我们把一块金子敲变形之后,虽然形状有所改变,但质量却没有发生变化,也就是说金子外观发生了变化但是仍然被认可,这种特性呢被称为“可锻性”。
在比特币系统中,也有一个类似的名词,“ Transaction Malleability ”。这个词通常翻译为“交易延展性”或“交易可锻性”。交易延展性源于比特币源代码中的一个错误。这个错误,可以在不改变交易输出或交易内容的情况下,更改交易ID。这个错误意味着,在交易被矿工写入区块之前,交易签名可以被更改。换句话说,也就是一笔未被确认的比特币交易,有可能被黑客造出两笔合法的交易。利用交易延展性而造成的攻击就叫交易延展性攻击。
2、交易延展性攻击
2014 年,有人利用比特币的这个漏洞大规模攻击比特币网络,内存池中充满了假交易,造成比特币网络堵塞,导致部分全节点宕机,导致比特币网络极其不稳定。曾经最大的比特币交易所——Mt.Gox宣布倒闭的部分原因,就是延展性攻击。
举个例子:小黑从交易平台发起提币,然后他提币这个交易就被广播出去了,在他这笔交易还没有被节点验证之前,小黑进行了延展性攻击,恰好攻击产生的新交易先被确认,而新交易照样会让他获得币(就像金子外观变了一样被认可),但是贪心的小黑却向交易平台申诉自己并没有收到币,交易平台一看,之前给小黑转币的那笔交易确实被拒绝了,因此又给小黑汇了一笔币,小黑心里美滋滋。贪心的小黑还不满足,他又以同样的攻击方式继续多次攻击,这样就导致了交易平台的资金大量流失。
2.1 两种攻击方式:
- 更改交易的签名
signature(r, s) 等价于 signature(r, -s(mod N)),BIP66 后规定签名算法必须是 DER-encoded ASN.1 - 更改解锁脚本 UnLock Script ,如增加一些无关指令(如 OP_DROP)不影响执行结果
2.2 可能导致的后果:
- 双重支付:攻击者可以更改交易ID,从而在同一时间内向不同的地址发送相同的比特币,从而实现双重支付。
- 交易拒绝:攻击者可以更改交易ID,从而使得交易被拒绝,导致比特币网络的拥堵。
- 交易混淆:攻击者可以更改交易ID,从而混淆交易的来源和目的地,从而使得交易难以追踪和确认。
二、SegWit 技术细节
在开始之前,我们先要简单回顾一下比特币的支付系统。它并不像银行那样,是一套账户和余额的列表。相反,每个比特币地址的余额都是由一系列发送给这个地址的交易来表示的;交易这一数据结构的主要部分就是输入和输出。输入是我们想要花费的前序交易(准确来说,输入不会是完整的一笔交易,而是某笔的输出,因为我们可能会在一笔交易中将资金转往多个地址),而交易的输出就是我们的资金发送的目的地址。下图展示了比特币交易的结构:
- Signature Script 字段,简称解锁脚本 “scriptSig”,它是用来打开锁定脚本的钥匙,是用来证明地址所有权的。
- 输出中的 PubKey Script 字段,简称锁定脚本“scriptPubKey”,用来保证只有接受地址的所有者才能使用这个支出。
1、SegWit 设计思路
- 将交易数据和签名数据分离存储,解决比特币的交易延展性问题。
- 通过重新组织区块数据,将签名数据存储在一个单独的区块中,提高比特币的交易吞吐量。
- 支持更复杂的交易脚本,例如多重签名和脚本哈希锁定等。
- 通过引入新的交易输出类型,例如 P2WPKH和 P2WSH ,使得比特币网络更加灵活和可扩展。
2、四种交易结构
2.1 Segwit 前:
- P2SH 是 Pay-to-Script-Hash 的缩写,是一种比特币交易输出类型。P2SH的交易输出脚本是一个包含脚本哈希的锁定脚本,其中脚本哈希是一个由多个操作码组成的脚本的哈希值。P2SH 的交易输出可以通过提供一个与脚本哈希相对应的解锁脚本来解锁。P2SH 的主要优点是可以支持更复杂的交易脚本,例如多重签名和脚本哈希锁定等。
- P2PKH 是 Pay-to-Public-Key-Hash 的缩写,是一种比特币交易输出类型。P2PKH 的交易输出脚本是一个包含公钥哈希的锁定脚本,其中公钥哈希是一个由公钥生成的哈希值。P2PKH 的交易输出可以通过提供与公钥相对应的签名来解锁。
2.2 Segwit 后:
- P2WSH 是 Pay-to-Witness-Script-Hash 的缩写,是一种基于 SegWit 的新类型的比特币交易类型。P2WSH的交易输出脚本是一个包含脚本哈希的锁定脚本,其中脚本哈希是一个由多个操作码组成的脚本的哈希值。P2WSH 的交易输出可以通过提供一个与脚本哈希相对应的解锁脚本来解锁。P2WSH 的主要优点是可以支持更复杂的交易脚本,例如多重签名和脚本哈希锁定等,并且可以降低交易费用。
- P2WPKH 是Pay-to-Witness-Public-Key-Hash的缩写,是一种基于 SegWit 的新类型的比特币交易类型,是比特币 P2PKH 的改进。P2WPKH 的交易输出脚本是一个包含公钥哈希的锁定脚本,**公钥哈希是见证程序的哈希值。**P2WPKH 的交易输出可以通过使用新的 OP0 操作码来解锁,这个操作码是 SegWit 的一部分。P2WPKH 的交易输出可以被认为是比特币的“新地址”,它们以“bc1”开头,而不是以“1”或“3”开头,这使得它们更容易识别和区分。
3、交易结构详解
3.1 Pay-to-Witness-Public-Key-Hash
我们用例子来说明一下隔离见证会如何改变交易的数据结构。从标准的 Pay-to-Public-Key-Hash (P2PKH) 交易类型开始。
我们感兴趣的部分是输出,尤其是其 “scriptPubKey” 字段(锁定脚本字段)。我们先考虑一种标准的锁定脚本:
OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
而隔离见证之后的锁定脚本如下所示:
0 <PubKeyHash>
如你所见,隔离见证的输出比传统类型的输出要简单很多:只有两个值会被推入脚本执行栈中。第一个数字解释为版本号,第二个则对应着一个锁定脚本(witness 程序)。旧版本的比特币客户端会以为这个输出是掉在地上的钱 —— 无需提供签名就能花费这个输出。
需要注意的是,虽然传统的客户端可以处理隔离见证的交易(他们会把这些输出当成人人都可以花的钱),但他们自己没法花这些钱:旧型的钱包可能会尝试用空签名来花用一个隔离见证的输出,但这笔交易在现实中是无效的(更新之后的节点不会允许这样的交易上链)。这就意味着,发送者必须知道接受方的钱包支不支持隔离见证,这样才能为之创建合适类型的输出。
再来看看这个输出被花费时的情形。传统交易的输出在花费时的数据结构如下:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": "<our scriptSig>"
}
]
[...]
但是,在花费一个隔离见证输出的时候,交易的 scriptSig 将为空,而所有的签名都会放到一个专门的地方:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": ""
}
]
[...]
"witness": "<Witness data>"
[...]
由 BIP 143 定义,隔离见证的输出应该用 压缩公钥 的哈希值来创建。如果你用的是传统类型的地址或者非压缩公钥的哈希值,这个输出将变得不可用(币会锁死)。
先做 SHA256 哈希运算,再做 RIPEMD160 运算,就可得到一个 20 字节的哈希值,进而得到正确的地址。
3.2 Pay-to-Witness-Script-Hash
另一个关键的交易类型是 P2SH 。它让交易可以发送给脚本的哈希值(而非公钥的哈希值,也即比特币地址)。要花费 P2SH 交易的输出,花费者需要提供一个脚本(叫做 “赎回脚本”),其哈希值应该与 UTXO 中的脚本哈希值匹配,并基于这个脚本提供 签名/口令/别的东西 。这个用法可以把解锁脚本保护起来,让发送者无从知晓一个地址的内容,并且也能节约空间:举个例子,一个多签名钱包的锁定脚本可能非常长,这样我们就必须把整个锁保存起来;有了 P2SH 可以只保存一个哈希值。
假设现在有一个需要提供 5 个私钥中的 2 个的签名才能使用的多签名钱包。如果你使用传统的交易,P2SH 交易输出的锁定脚本将如下:
HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL
要花它的时候,花费的人(也是上一笔交易的接收方)需要提供一个赎回脚本,这个脚本定义了花费条件(多签名,2-5),还有两个签名。所有这些都要放在交易的输入中:
[...]
"Vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": "<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>",
]
再来看看使用隔离见证后的发送者和接收者。输出的锁定脚本如下:
0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
就像 P2PKH 交易一样,这个输出的脚本也变得更简单。第一个数值表示版本号,第二个是对应于赎回脚本(witness 程序)的 SHA256 哈希值(32位)。使用这个函数某种意义上是为了用长度来区分 P2WPKH 的见证程序以及 P2WSH 的见证程序(32 字节的 SHA256 哈希值 vs. RIPEMD160(SHA256(script)))。
使用这一输出的交易如下所示:
[...]
"Vin" : [
"txid": "abcdef12345...",
"vout": 0,
"scriptSig": "",
]
[...]
"witness": "<SigA> <SigB> <2 PubA PubB PubC PubD PubE 5 CHECKMULTISIG>"
[...]
4、在 P2SH 中嵌入隔离见证
我们已经看到,使用隔离见证是有好处的。不过,上面的例子只对发送者和接收者都有升级软件的情形才适用。但现实并不总是如此。考虑这样一种情形:
Alice 希望给 Bob 转账一些 btc,Bob 有支持隔离见证的钱包软件而她没有。他们显然只能用标准形式的交易,但 Bob 希望使用 SegWit 来减少手续费。这时候,Bob 可以创建一个包含了 SegWit 脚本的 P2SH 地址、Alice 会把这个地址当成一个普通的 P2SH 地址,因此可以直接向这个地址转账而没有任何问题。但 Bob 可以使用 SegWit 交易来使用这个输出,并获得手续费折扣。
这就是 SegWit 交易的两种类型 P2WSH 和 P2WPKH 在 P2SH 内实现的方式。
4.1 P2SH(P2WPKH)
想在 P2SH 交易中实现一笔 P2WPKH 交易,Bob 需要使用其公钥创建一个见证程序。创建 P2SH(P2WPKH) 见证程序的流程如下:
- 生成公钥:首先需要生成一个公钥,这个公钥可以是比特币地址的公钥,也可以是其他类型的公钥。
- 创建见证程序:将公钥转换为见证程序,这个过程可以使用 BIP141 中定义的公钥哈希算法进行计算。
- 计算哈希值:将见证程序进行哈希计算,得到一个 20 字节的哈希值。
- 创建 P2SH(P2WPKH) 地址:将 20 字节的哈希值添加到 P2SH(P2WPKH) 地址的锁定脚本中,得到一个 P2SH(P2WPKH) 地址。
第一个数值是版本号,而第二个数值是 20 字节的公钥哈希值。这个脚本先做 SHA256 哈希运算,再做 RIPEMD160 运算,就可得到一个 20 字节的哈希值。
0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7
这个 P2WPKH 见证程序的 HASH160 结果:
3e0547268b3b19288b3adef9719ec8659f4b2b0b
转化成一个地址:
37Lx99uaGn5avKBxiW26HjedQE3LrDCZru
发送给这个地址的输出的锁定脚本,看起来也就跟一个普通的 P2SH 地址的脚本没啥区别:
HASH160 3e0547268b3b19288b3adef9719ec8659f4b2b0b EQUAL
那么 Bob 花费输出的时候,交易的结构会像这样:
[...]
"Vin" : [
{
"txid": "8adbca5e652c68f8f3c30ac658115bc4af395d0cc7e6beaea18168295c29d011",
"vout": 0,
"scriptSig": "0 ab68025513c3dbd2f7b92a94e0581f5d50f654e7"
}
]
[...]
"witness": "<Witness data>"
[...]
在一开始,我们创建的赎回脚本(也就是那个见证程序)会经过一次哈希计算,如果结果符合锁定脚本中的哈希值,这个脚本就会得到执行,程序会验证放在 witness 字段的签名。
4.2 P2SH(P2WSH)
P2WSH 脚本也可以用 P2SH 来实现。我们考虑上面所说的 2-5 多签名钱包的例子。所有的步骤都跟 P2SH(P2WPKH) 没什么区别:
第一个数值是版本号,第二个数值是 32 位的 SHA256 哈希值,对应于我们的签名脚本。
0 9592d601848d04b172905e0ddb0adde59f1590f1e553ffc81ddc4b0ed927dd73
然后我们拿这个见证程序的 HASH160 哈希值转成一个普通的 P2SH 地址。要使用发往这个地址的输出时,我们需在 scriptSig 字段公布这个见证程序,在 witeness 字段提供完整的多签名脚本。
三、Segwit 落地应用
1、SegWit 的优点
1.1 后向兼容性
实际上,隔离见证不仅改变了交易的结构,也改变了交易的输出。不过,这不是说传统类型的 UTXO 和 SegWit 类型的 UTXO 无法在同一笔交易中花费:这种情况下,传统类型的 UTXO 将在输入(脚本签名字段)内加载所有权证明,而隔离见证类型的 UTXO 将在交易输入以外的结构中加载证明。
不管怎么说,隔离见证的定位是一个软分叉,这个升级应该是可以忽略,无需强制的,而且,这也意味着,未升级的节点应该可以处理隔离见证类型的输出。实际上,旧的节点和钱包将以为任何人都能花费这些 UTXO,也即这些 UTXO 是空签名也可花费的,因此即使在交易中没有看到签名,交易也仍然是有效的。而升级后的节点和钱包将在交易输入以外的地方,一个专门的 “witness” 字段寻找签名。
1.2 交易熔融性漏洞
SegWit 解决了比特币交易的 “熔融性”,因为所有的签名都是放在交易外面的,因此签名的变动不会导致交易的哈希值变动,也就不会影响交易的 ID。隔离见证还引入了一个专门的标识符,叫做 “wtxid”:它是交易和整个 witness 部分的哈希值,所以如果一笔交易在传播时没有附带任何 witness 数据,交易 ID(txid)就等于 wtxid 。
这个解决方案使得我们可以创建一系列前后相继的未确认交易,而无需担心任何风险,这对闪电网络这样的协议来说是非常重要的。(如果不解决交易熔融性问题,支付通道的参与者就无法快速检索对手有没有把一笔过时的通道交易上链,因为同样内容的交易可能会以完全不同的 ID 出现)。
1.4 网络和存储的扩展
Witness 数据往往是交易数据中占比最大的一部分。在使用多签名脚本的交易中,witness 最多可能占据交易数据量的 75%。感谢 SegWit,签名的传输变成了一个可选项:只有节点想要验证交易时,才需要请求这些数据。而没有支持 SegWit 的 SPV(简易支付验证)客户端和节点也无需下载额外的数据,可以节省硬盘空间。
1.5 可用的区块空间扩大,降低交易手续费
SegWit 类型的交易比以往的交易类型更便宜,因为它减少了需要存储的 witness 数据。准确来说,“Size”(数据量大小)的概念在 SegWit 类型的交易上略有不同。它引入了一个 “虚拟大小(virtual size)” 的概念:所有放在 witness 部分的数据都会乘以 0.25 来计算数据量大小,从而一个区块中可以塞进更多的交易。来看一个例子。
假设我们有一笔传统类型的交易,数据量大小为 200 字节。那么 1MB 的区块里面可以放进 5000 笔这样的交易。而一笔等效的 SigWit 交易有 120 字节是放在 witness 区域的,因此其虚拟大小为 80 + 0.25 * 120 = 110 字节,所以区块可以放入 9090 笔这样的交易。如果上链的手续费是每字节 40 聪,则交易费会从 8000 聪减低到 4400 聪,几乎打了个对折。(译者注:“聪” 为比特币的数量单位,是 btc 的亿分之一。)
1.6 脚本版本
你可能已经注意到了,每个锁定脚本都会有 1 个字节来表示脚本的版本。使用不同的版本号就能以软分叉的形式增加或变更功能(语法改变、新的操作符,等等)。
1.7 签名验证的效率优化
在传统的比特币交易中,签名数据需要与交易数据一起进行散列计算,而签名数据的大小与交易中的输入数量成正比,因此,如果交易中有 n 个输入,每个输入都有一个签名,那么哈希计算的次数将是 n^2 次。这种计算方式会导致签名数据的重复计算,从而降低了签名验证的效率。而隔离见证通过将签名数据从交易数据中分离出来,可以避免这种重复计算,从而提高了签名验证的效率。
2、SegWit 存在的问题
如果百利而无一害,怎么还有人会觉得有问题呢?比特币社区有许多人反对这一升级,因为,即使它有这么多长处,它也有一些缺点。我们来看看反对方提出的一些意见。
- 因为 SegWit 是一个软分叉,许多客户端可能不会升级,因此两种类型的 UTXO 会在网络中同时存在;诸如消除交易 ID 熔融性以及哈希计算次数线性上升这样的重大变更对非 SegWit 输出无效,因此网络仍会暴露在交易 ID 熔融性和哈希时间平方级上升的风险中。
- SegWit 会降低网络的安全性,执行完全验证的节点会大幅减少,因为只有那些适配了 SegWit 的节点才有能力验证交易的 witness 部分。
- SegWit 不能被废除。如果废除了它,所有变更都撤销,那么所有的 SegWit 输出就会变成大街上任人捡拾的钱。
- SegWit 希望一次解决所有问题,也正因此,它导致了大量的代码改动。它会让未来的工作更加负载,而且提高了出现驱之不去的软件 bug 的机会。
3、应用案例
- 闪电网络:闪电网络是一种基于比特币的二层扩容方案,它利用隔离见证的技术,将比特币的交易数据从区块链上移除,从而实现了快速、低成本的比特币交易。
- Taproot:Taproot 是一种基于隔离见证的新的比特币交易输出类型,它可以支持更复杂的交易脚本,并且可以降低交易费用。Taproot 还为比特币的 Ordinals 提供了技术支持。
- Ordinals:Ordinals 是一种基于比特币的 NFT(非同质化代币)协议,它利用隔离见证的技术,将 NFT 的元数据存储在比特币的交易输出中,从而实现了去中心化的 NFT 发行和交易。
参考
详尽解释隔离见证:详尽解释隔离见证
隔离见证 SegWit :https://www.youtube.com/watch?v=qGC490LmYjs
隔离见证的好处:隔离见证的好处
隔离见证的代价与风险:隔离见证的代价与风险
对隔离见证交易重量的误解:对隔离见证交易重量的误解