首页区块链技术智能合约BTC地址与交易原理深度剖析,你应该知道
  • 评论(2)

分享到

微博

微信

QQ

BTC地址与交易原理深度剖析,你应该知道

 A & 比特币地址有 1 打头地址 ,也有 3 打头的地址,这两者有什么区别吗? 在哪种情况下,地址上的比特币会被锁死? 到底是谁拥有比特币的控制权,是你?还是你的钱包?

如果你在使用比特币钱包,但却无法回答上面三个问题,那么这篇文章是为你而写。

笔者在对数字钱包源码审计时,发现一个名为 pywallet 的比特币钱包开源库包含了一个严重缺陷。如果向 pywallet 生成的 OmniLayer 收款地址转账,将导致资产永久丢失。

据笔者区块链技术专家 zer0to0ne 解释,OmniLayer 协议允许在比特币区块链上发行自定义资产(比如 USDT)。OmniLayer 资产交易的本质是比特币交易。比特币交易的代码库有很多,pywallet 便是其中一种。它可以方便的构造符合 OmniLayer 格式的比特币交易。目前 pywallet 已经被应用在一些数字钱包软件中。

但是,开源库 pywallet 在生成 OmniLayer 钱包地址的时候,误将地址的前缀写反了,若干资产被锁死在无效的地址内!

下面是 pywallet 相关错误代码截图:

BTC地址与交易原理深度剖析,你应该知道

文件地址:https://github.com/ranaroussi/pywallet/commit/eb784ea4dd62fe2a50e1352e7d24438fc66a4ac0#diff-ca3a8be6f2ab4be3bfd69a49f5f4122a

插队科普一下:比特币网络上最常见的地址类型有三种:普通公钥地址(1-地址),脚本哈希地址(3-地址)和隔离见证地址(bc1-地址),地址类型通过地址的前缀来区分。其中1-地址 的前缀为0x00,3-地址 的前缀为0x05。

  • 1-地址:这是最常见的比特币地址,通常用于普通转账收款。1-地址 实际上为公钥Hash的编码。验证 1-地址 的签名后便可解锁收款。

  • 3-地址:这个地址为脚本(Script)哈希地址。这类地址实际对应为一段比特币脚本Hash的编码。

  • bc1-地址:bech32编码地址,用于隔离见证交易。

开源库 pywallet 颠倒了地址前缀,将1-地址 错误地设置为 3-地址。因此原本要转给 1-地址 的资产会误转入 3-地址。当账户持有者以 1-地址 的验证方式,也就是私钥签名去取出资产的时候,区块链网络却以 3-地址 执行脚本的方式去执行验证,导致用户无法正常取出资产!

请慎重使用 pywallet 开源库!!

真相:比特币从未真正实现过转账功能

这一点出乎很多人的意料,由于比特币的实现基于 UTXO 模型,与我们直观理解的账户模型不一样。zer0to0ne 解释说,实际上比特币从未真正实现过通常意义上的转账功能。中本聪只给比特币设计了一系列比特币脚本操作符和比特币脚本执行器,而所谓的转账过程实际是由一段比特币脚本锁定、解锁过程来模拟。这与日常生活中的账本概念(或称之为账户模型)不一样。

为了便于理解,我们可以把比特币区块链上的资产交易比喻成 将资产锁进保险箱,只有持有保险箱钥匙的人(即收款人)才能拿出保险箱中的资产进行交易。举个例子,如果 Alice 要向 Bob 支付一笔资产,Alice 将这笔资产锁进一个保险箱中,只有 Bob 才有这个保险箱的钥匙,即只有Bob才能取出这笔资产。如果 Bob 要取出资产,那么要求 Bob 必须同时花掉这笔资产(即锁入另一个保险箱)。在 Bob 没有取出资产前,资产并不真正属于 Bob。设想如果 Bob 丢了钥匙,那么将无法再取出资产。 换句话说,这笔资产还在保险箱中保存的时候,既不属于Alice,也不完全属于Bob。当然,Alice 也可以把资产放入任何人都可以打开的保险箱中,这也被称之为 Anyone-Can-Spend 交易。

由于比特币区块链上的收款地址不同,保险箱的类型也有所不同。不同类型的保险箱需要不同类型的钥匙来开启。付款人为收款人定制一个保险箱,将资产放入保险箱中并上锁,再将保险箱丢到公共场所。而保险箱有两种开启方法:

  • 若收款人为1-地址,我们称保险箱为1-类保险箱 。而相应的钥匙必须是指定收款地址对应的私钥。解锁保险箱的过程也就是验证 1-地址 公钥以及公钥对应的数字签名,这也是我们通常所理解的向普通账户地址转账的验证过程。

  • 若收款人为 3-地址,我们称为3-类保险箱 。开启钥匙必须为一段可以执行的比特币脚本。解锁保险箱的过程是:比特币脚本的Hash值对应到 3-地址 ,同时比特币脚本执行器运行该脚本后成功返回。也就是说只有拥有脚本原文并可以成功执行的人才可以提取这个保险箱里的资产。

回到这一节的问题:为什么说比特币从未实现真正意义上的转账功能。答案很简单,因为比特币系统中根本就不存在账户的概念,账户之间的转账也无从谈起。一个人能在未来打开多少个保险箱,也是未知数。

通过上面的解释,我们可知:当 pywallet 开源库误将 1-地址 识别为 3-地址 时,就好像将原本的 1-类保险箱 改造成了 3-类保险箱,而账户持有者还是拿着 1-类保险箱 的钥匙去解锁,那么自然无法打开保险箱。那么之前 zer0to0ne 发现的被误锁住的 OmniLayer 数字资产是否能恢复?

是否存在一种可能性,采用 1-地址 的钥匙去开启 3-保险箱 ?

zer0to0ne 接着向我们详细解释了两个重要概念 P2PKH(Pay to Public Key Hash) 与 P2SH (Pay to Script Hash)的来龙去脉。这两个名词分别代表了两种不同的比特币交易类型。

下面是 zer0to0ne 的精彩技术细节分析 

P2PKH: 中本聪的伟大发明

Pay to Public Key Hash 顾名思义,是将比特币放入一个保险箱,钥匙孔为公钥 Hash(Public Key Hash)。我们最常见到的 1-地址 本质上就是 Public Key Hash 的一种编码。1-地址 的生成过程也很简单,将公钥经过Hash160运算得到 Public Key Hash,在 Public Key Hash 头部补上前缀 0x00,Hash 尾部补上校验和,经过Base58便得到了1开头的比特币地址。

Base58(0x00 + <Public Key Hash> + Checksum)

我们来看看P2PKH交易类型的保险箱构造过程,Alice发送比特币给Bob为例:付款方 Alice 在构造保险箱的时候需要设置一个锁定脚本:

OP_DUP 
OP_HASH160 
(Bob收款地址蕴含的Public Key Hash) 
OP_EQUALVERIFY 
OP_CHECKSIG

注:我们可以把这一步理解为 Alice 为 Bob 定制了一个保险箱,把比特币放入保险箱并用 Bob 的公钥 PubKey Hash上锁。现在这把锁除了持有私钥的 Bob,谁都无法打开。

当 Bob 需要花费 Alice 给他的比特币时,需要提供必要的参数:交易签名 + 公钥(技术黑话:scriptSig)来开启保险箱,使得锁定脚本执行后返回 True,这一步通常由钱包自动完成。

我们来看看比特币节点是如何校验 scriptSig 合法性的。

BTC地址与交易原理深度剖析,你应该知道

图片来自Mastering Bitcoin

脚本执行过程如图所示,Bob将交易签名后得到的数据(实际上还要包含数据长度信息),真正的scriptSig 应该为,比特币脚本执行器从 PUSH 数据开始,PUSH 操作会读取第一个字节获取将要入栈的数据长度信息,然后持续执行比特币脚本,直到最后执行完毕检查执行结果。

首先入栈的是 ,然后将 入栈,一次DUP操作将在栈顶复制一份 ,HASH160弹出栈顶的 并计算Hash,将结果压回栈中,之后使用 EQUALVERIFY 弹出Hash对比是否和相等,如果相等则返回True,不相等便标记交易为无效。执行到这一步,暴露了公钥,确保了签名者的身份的正确性,但是黑客或矿工可以通过暴露的公钥构造出一个新的交易替换原始交易,无法保证安全,那么便需要下一步来保证交易无法伪造。此时栈上还有 和 ,执行 CHECKSIG,将校验数字签名的正确性,确保了签名者拥有地址对应的私钥。

数字签名除了持有私钥的人,谁也无法伪造,执行至此,一笔比特币P2PKH交易已经安全地完成了。

再解释一遍:当 Bob 要花费 Alice 给他的比特币时,Bob 只有用正确的钥匙才能打开 Alice 留给他的保险箱,把钱放入 Bob 新构造的一个保险箱里。

这时候一些聪明的读者会注意到一个细节:如果 Bob 取出钥匙,在还未打开保险箱的时刻,区块链上的任何矿工都能看得见这把钥匙的形状,理论上他们是可以立即复制一把钥匙,把 Alice 留给 Bob 的保险箱打开并花掉(俗称 Front-running 攻击)。真的可以这样做吗?显然中本聪考虑了这个问题,这把钥匙中的交易签名是 Bob 发起的交易的完整签名。假设 Bob 要将 Alice 构造的保险箱中的比特币装入一个新的保险箱(留给Charlie),这时候 Bob 出示的钥匙包含了 Charlie 的公钥Hash,矿工虽然可以复制 Bob 的钥匙,但是这把钥匙已经隐藏了下一个新保险箱的关键信息,因此矿工无法使用这个复制钥匙来完成别的动作(无法挪用数字签名)。

P2SH: 后中本聪时代的重大创新

中本聪设计了一个这么强大的脚本系统,只用来构造转账交易似乎太浪费了,我们试试用其他指令构造一些特别的锁定脚本,并使用其他方式来解锁。

例如我们可以构造一个用 Hash 原象(Pre-image)来解锁交易的脚本:

OP_HASH160
OP_EQUAL

这个脚本的含义是:当满足 Hash160(Pre-image)==这个条件时,便可成功将脚本解锁。

我们继续通过保险箱的例子来解释,并给这类保险箱起名为3-类保险箱。现在 Alice 给 Bob 的比特币锁定在一个由上述 Hash160保护的保险箱里,我们姑且称之为哈希锁吧。

这把锁依然需要正确的形状才能开启,但是安全性却弱很多,缺少数字签名机制导致钥匙隐藏的关键信息不会随着Bob 新建的保险箱而变化。任何矿工都能在 Bob 亮出钥匙的一瞬间复制出一摸一样的钥匙,抢着去开Alice留给Bob的保险箱(Front-running),将币转给另一个人 Eve,于是原本属于 Bob 的比特币会被洗劫一空。

虽然这个脚本非常不安全,但是它却有两个非常神奇的功能:

交易构造的输出足够短,意味着比特币节点维护的 UTXO 缓存占用空间将会大大减小

  1. Pre-image 总是在交易被花费时作为 input 来引用,不会在交易的 output 侧出现,UTXO依然保持精简,同时可以把手续费负担转嫁给接收方。

  2. 既然所述的输出脚本好处很多,那我们是否有办法让这种交易方式变得安全呢?这就需要讲讲什么是 P2SH了。

比特币核心开发者 Gavin Adresen 提出了一种叫做 Pay to Script Hash (P2SH) 的技术。

P2SH 的交易输出依然是判断 Hash160(Script)==

©免责声明和风险提示:本文系用户自行发布或转载,不代表比特万象任何观点,如有任何形式的转载请联系原作者。文章中的所有内容均不构成比特万象任何的投资建议及意见、立场,请您根据自身评估做出理性决策。比特万象仅提供网络存储空间服务,如文章侵犯到您的合法权利,请您通知比特万象予以删除。
粤ICP备17084271号-2 Copyright © 比特万象 版权所有