区块链技术 -- 比特币隔离见证地址 与 延展性攻击

3,132 阅读9分钟

作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。

掘金:juejin.im/user/178526…

博客:www.cnblogs.com/linguanh/

GitHub : github.com/af913337456…

腾讯云专栏: cloud.tencent.com/developer/u…


提示:阅读本文需要一定的比特币技术基础知识

目录

  • 背景简介
  • 协议
  • 隔离见证地址的生成
  • 扩容原理
  • 交易的验签
  • 延展性攻击
    • 场景
    • 疑问与解答
    • 修改S的代码

背景简介

隔离见证源于比特币的一次升级,这次升级涉及到了共识的层面,它支持多了一种交易模式 --- 隔离见证交易,也因此导致了一次软分叉。

它出现背景是为了解决下面两个问题

  1. 因为椭圆曲线签名算法ECDSA的漏洞导致的比特币“延展性攻击”问题;

  2. 在一定程度上达到比特币区块扩容的目的。

协议

要注意,“隔离见证地址” 是比特币 “隔离见证体系” 中的一部分,整个隔离见证体系是由多个部分组成的。完整体系的介绍在比特币的改进协议(BIP)里面,主要由下面的协议文档共同参与:

  1. BIP-141,链接是:github.com/bitcoin/bip… ,141对隔离见证做了详细的介绍,包含其定义和用途;

  2. BIP-143,链接是:github.com/bitcoin/bip… ,143对版本号为0的隔离见证,在交易中的,其签名,和验签的整体流程做了详细的介绍;

  3. BIP-144,链接是:github.com/bitcoin/bip… ,144 对隔离见证在点对点(P2P)网络中,是如何被操作参与的,做了详细的介绍;

  4. BIP-173,链接是:github.com/bitcoin/bip… ,173里面对隔离见证地址做了介绍,包括它是使用什么的编码格式,校验码是怎样的等等。

其他的更多关于隔离见证的官方介绍,可以自行查看完整的所有改进协议,链接是:github.com/bitcoin/bip…

地址的生成

下面我们来认识下隔离见证地址是如何生成的。 比特币的地址生成步骤,都是从公钥开始进行的,需要经过多个字节拼凑再进行hash算法编码的过程。同样地,隔离见证地址的生成流程也是类似的,参照BIP-173协议,我们可以总结出它的生成流程:

  1. 准备好比特币中,解锁脚本中的16进制哈希字节流,目前主要有两种,分别是P2WPKH 和 P2WSH。这两种脚本它们最明显的区别是:P2WPKH 中的哈希占了20个字节,而P2WSH中的哈希是32个字节,对应的脚本结构分别是:
  • P2WPKH:OP_0 <20-byte哈希>
  • P2WSH:OP_HASH160 <32-byte 哈希> OP_EQUAL
  1. 选择好不同比特币网络所对应的“hrp”字符串,分别有,主网:“bc”,测试网络:“tb”,私人regtest网络:“bcrt”。这些信息定义在源码中的配置文件里面,比如Go版本的路径是:github.com\btcsuite\btcd\chaincfg\params.go

  2. 将步骤1的字节流使用5位一个字节进行编码,原本的是8位一个字节,结果设为 B;

  3. 将0x00字节添加到 B前面,结果设为 C;

  4. 使用“bech32”的生成校验码算法对hrp 和 C 的字节流生成校验码 D;

  5. 将D添加到C后面,结果设为 E ;

  6. 组装:hrp + "1" + E结合编码表的映射字符串,得到地址。

其中,步骤一虽然不是公钥直接参与生成,但其中的哈希数据也是由公钥经过演变来的。第七步骤的“bech32”编码表字符组合是:qpzry9x8gf2tvdw0s3jn54khce6mua7l。因为不同的脚本中的哈希结构的字节数不一样,所以结果也是不一样的。下图是上面生成步骤对应的流程图:

地址生成的代码实现可以直接使用btcd源码中提供的函数,如下图所示:

扩容原理

在比特币交易的一般打包流程里,是会把每笔交易的签名数据也包含进去的,如下图所示。

同时我们知道,一个区块所能容纳的数据量大小是有限的,意味着一个区块所能打包的交易数也是有限的,如果我们能够想办法把交易里面的数据量减少,那么就能间接地达到了区块扩容的目的。

而隔离见证的本质含义就是在区块打包交易的时候,不打包签名的数据,把签名数据放到另外一个地方去。如果是这样,那么怎样验证交易的签名是否正确,以保证数据没被篡改?

隔离见证交易的验签

做法是这样的,在将要发起交易,构造脚本的输入Vin时,把输入的解锁脚本数据放置到另外一个字段处,源码中,这个字段的名称是:Witness。当交易被发送到交易节点的时候,节点代码中会从这个字段里面提取出数据恢复出解锁脚本,接着在节点进行验签操作,验签通过后,签名的数据就不再被打包进区块里面,后续要消费这边交易的时候,解锁脚本也是从Witness字段中恢复。

而不是隔离见证的交易类型,Witness字段是没有数据的,解锁脚本的数据被携带在签名里面,导致要恢复,只能从签名中恢复,意味着签名数据必须要被打包在区块里面。下面是Vin的和上述相关的结构:

type Vin struct {
    // 省略无关字段
    ScriptSig *ScriptSig  `json:"scriptSig"`
    Witness   []string   `json:"txinwitness"`
}

ScriptSig 放置的就是签名数据Witness 放置的就是解锁脚本数据。源码中的脚本恢复操作函数在比特币操作码虚拟机部分,如下图所示。

延展性攻击

上面说到了隔离见证的出现背景之一是为了解决“延展性攻击”,这个攻击又被称为“可锻性攻击”,英文名称:transaction malleability attack。

2014年,MT.GOX交易所(门头沟)发生的85万个比特币丢失事件,事后当事人把此次事故,归罪于比特币的交易延展性攻击。下面我们来看看它是怎样达到攻击目的的。 在比特币中,区分一笔交易的凭据是交易的id,即TxId。如果两笔交易的TxId不一样,那么会被认为是两笔不同的交易。在我们日常使用区块链浏览器去查看交易的时候,也会根据TxId去查询交易,两个不同的TxId,就肯定不是同一笔交易。

现在假设这么一个场景:

A 使用椭圆曲线签名算法ECDSA发送了一笔交易T去比特币节点N,此时N会根据T的id校验T是否已经存在交易池里面及其相关逻辑,如果发现已经存在且不满足替换条件,那么就会返回错误信息给 A。如果不存在,就会被放置到交易池里面,等待被处理,同时返回了 T 的 id 给A。 此时假设 B 自己编写了个程序,对比特币节点交易池进行了监听,发现了交易 T,便将 T 提取了出来,并获取了 Vin 结构中的ScriptSig 字段。 上面场景对应的流程图如下图所示。

B 获取了T 的 ScriptSig 后,要做什么呢?首先可以肯定的是,B 肯定不能篡改 T 的数据,那么他要怎样进行“延展性攻击”

B 接下来这样做:

B 将ScriptSig 使用ECDSA的相关代码恢复出签名信息的 R 和 S 整形大数。然后根据椭圆曲线加密算法的漏洞修改了 S,再重新生成签名的 ScriptSig,然后将这笔交易重放。注意这里!重放的时候,节点N 会重新根据整笔交易的信息,包含 ScriptSig 生成一个TxId_2,但是因为 ScriptSig 被改过了,所以导致了 TxId不一样,因为TxId 是使用hash 算法生成的,hash 算法在不同的输入情况下,输出是肯定不一样的。

B 操作部分的描述留下了两个疑问:
  1. 相同交易的发到链上,输入部分(UTXO)不会被检测吗?

  2. 为什么 ScriptSig 被改过了,还能验签成功?

回答:
  1. 因为比特币的账户模型是基于UTXO的,如果某笔 UTXO 还没被消费,那么它可以被尝试双花。而当它已经被消费了,此时再消费,就会出错。在上面的例子中,交易T及其被 B 修改过的复制版本中的UTXO,虽然是一样的,但是它们都是处于没被花费的,所以可以被多次引用。即检测,只检测是否已被上链花费了。

  2. 之所以能验签成功,是因为椭圆曲线的签名算法ECDSA中,对于 S 和 R 可以验签,负S 和 R 也可以验签成功。但一个负S 却导致了 TxId的不一样。 B 的攻击流程图如下图所示:

至此,节点中同样的交易内容,却出现了不同的 TxId,交易的手续费和收款人完全相同。意味着,这两笔交易,具备都有被打包的可能,当其中一笔被打包了,另一笔就不会被打包,因为其中涉及的UTXO已经被花费了。

此时假设 A 是某交易所,而它此时正在帮用户提现,而B 是这个用户,A 在对账的时候,会根据自己发送交易时候拿到的TxId去核对,但它并不知道此时还有一个 TxId_2 是做了同样的提现操作。然后此时 TxId_2 被打包了,B 作为攻击者,发现自己攻击成功了,就会去向交易所说,自己的提现怎么还没成功。而A 会进行核实,发现自己的TxId失败了,然后就会重新给用户发起提现操作。 至此,B 收到了两笔或多笔链上的转账。

上面的整个过程,就是一次“延展性攻击”。

修改S的代码

在代码中的具体实现也是很简单的,只需要添加一行代码就能修改签名中的S,使得验签依然有效。下图是我实现的一个可行的函数,为了不被滥用,关键行数已打码

修改签名中的S 的代码: