BTC-比特币脚本
约 5331 字大约 18 分钟
2021-09-17
相关术语
The BitCoin Scripting Language 比特币脚本语言
confirmation 确认
stack based language 基于栈的语言
txid 交易id
P2PK Pay to Public Key
P2PKH Pay to Public Key Hash
P2SH Pay to Script Hash
RedeemScript 赎回脚本
Proof of Burn(PoB) 一种加密货币共识机制,它通过“燃烧”或永久性移除一定数量的代币来证明用户对网络的投入。
Sig 签名
CHECKSIG 验证签名,它的作用是把栈顶的俩个元素弹出来,用公钥检查这个签名是否正确,如果正确返回true。
CHECKMULTISIG 验证多重签名
AltCoin(Alternative Coin) 是指除了比特币(Bitcoin)之外的所有加密货币的总称
交易实例
这是比特币的一个交易实例。这个交易有一个输入,俩个输出。左边的output
其实是这个交易的输入,它的意思是说这个交易中使用的币是来自前面哪个交易的输出。右边俩个输出,下面那个已经花出去了,上面那个还没有花出去。这个交易已经收到了23个confirmation
确认,所以回滚的可能性很小。最下面是这个交易的输入和输出脚本,输入脚本包含俩个操作,分别把俩个很长的数压入栈里。比特币使用的脚本语言是非常简单的,唯一能访问的内存空间就是一个栈,不像通用的编程语言,有全局变量、局部变量还有什么动态分配的内存空间,它只有一个栈,所以叫基于栈的语言stack based language
。这里的输出脚本有俩行,分别对应上面的俩个输出,每个输出有自己对应的单独的脚本。
交易结构
这是交易的具体数据结构内容。
- txid 交易id
- hash 交易hash
- version 使用比特币协议的版本号
- size 这个交易的大小
- locktime 设置交易的生效时间。0表示立即生效,默认是0;如果是非0,就表示要等待多少个确认
- vin 输入部分
- vout 输出部分
- blockhash 这个交易所在区块的hash
- confirmations 有多少个确认
- time 交易产生时间
- blocktime 区块产生时间
输入部分
这是交易的输入部分,是一个数组。 一个交易中可以有多个输入,这里例子只有一个输入。每一项输入都要说明这个币是来自之前交易哪个的输出,所以前两行就是给出币的来源,
- txid 之前交易的hash值(币的来源的hash)
- vout 表示这个交易中的第几个输出
- scriptSig 输入脚本(同inputScrip)证明有权利花这个钱,如果一个交易有多个输入,每个输入都要说明币的来源,并且要给出签名,也就是说比特币中一个交易可能需要多个签名
输出部分
这是交易的输出,也是一个数组结构。这个例子中是有俩个输出,
- value 是输出的金额,给对方转过去多少钱,这里单位是比特币,如果很多数值可能是"聪"单位
- n 序号,表示是当前交易的第几个输出
- scriptPubKey 输出脚本(同outputScript),为什么这里叫scriptPubKey,输出脚本最简单的形式就是给出一个Public key
- asm 表示输出脚本的内容
- reqSigs 表示这个输出需要多少个签名才能兑现,例子中都只需要一个。后面会降到multi reqSigs,一个输出可能需要多个签名才能兑现
- type 输出类型,pubkeyhash即公钥的hash
- addresses 输出的地址
输入输出的执行
上面蓝色部分是一个小心的区块链,在前面第二个区块里有一个A到B的转账交易,B收到之后,在后面的区块里,有把币转给了C。也就是说,B给C的交易中,币的来源是来自于前面A转给B的这个交易。
所有下面tx
部分就能看到,B到C的txid
和vout
,指向的是A到B的交易的输出。那么要验证这个交易的合法性,是要把B到C的输入脚本,跟A到B这个交易的输出脚本拼接到一起来执行。
注意,这个执行操作有个交叉,前面这个交易的输出脚本放在后面,后面这个交易的输入脚本放在签名。在早期的比特币的实现中,这俩个脚本是拼接在一次,从头到尾执行一遍。后来处于安全因素的考虑,这俩个脚本改为分别执行,首先执行输入脚本,如果没有问题,再执行输出脚本。 如果能顺利执行,最后栈顶结果为非0时,也就是true
,表示验证通过,这个交易就是合法的;如果执行过程中出现任何错误,这个交易就是非法的。如果一个交易有多个输入的话,那么每个输入脚本都要和所对应的交易的输出脚本匹配之后来进行验证,全部通过表示交易是合法的。
接下来看一下输入和输出脚本的几种形式。
P2PK(Pay to Public Key)
输出脚本中直接给出收款人的公钥(output script
的第一行)。第二行CHECKSIG
是检查签名的操作。在输入脚本里直接给出签名就行,这个签名Sig
是用私钥对这个输入脚本所在的整个交易的签名。这种形式是最简单的。因为PubKey
是直接在输出脚本中给出的。
执行情况
这是P2PK
的输入脚本和输出脚本合并后的样子,三条命令。实际情况是输入和输出分开执行的,这里篇幅有限,就暂时看作顺序执行了。
第一条语句是,把输入脚本里提供的签名压入栈
第二条是把输出脚本里的提供的公钥压入栈
第三条,它的作用是把栈顶的俩个元素弹出来,用公钥检查这个签名是否正确,如果正确返回true
,表示验证通过。否则的话执行出错。
实例
这是P2PK
的一个实例,上面这个交易的输入脚本是把签名压入栈。下面这个交易,是上面这个交易的输入的来源,它的输出有俩行,第一行是把公钥压入栈,第二行就是CHECKSIG
。
P2PKH(Pay to Public Key Hash)
这种形式和上面的区别在于,输出脚本里没有直接给出收款人的公钥,给出的是公钥的hash
。公钥是在输入脚本里给出的。输入脚本既要给出签名,也要给出公钥。输出脚本里面多了一些其他操作,比如DUP
、HASH160
等,这些操作都是为了验证签名的正确性。这种形式实际是最常用。
执行情况
这个是把输入脚本和输出脚本拼接的结果。(当然为了方便学习,其实是分开执行的)。
前俩条语句是把签名和公钥压入栈
第三条语句,DUP
是把栈顶元素复制一遍,所以有多了一个公钥
第四个HASH160
,是把栈顶元素弹出来,取Hash
,再把得到的hash
压入栈,所以栈顶变成了公钥的hash
下面一句,是把输出脚本里提供的hash压入栈,现在栈顶就有了俩个公钥的hash
EQUALVERIFY
就是验证栈顶俩个hash
是否一致,如果相等就从栈顶消失了
最后一条CHECKSIG
。它的作用是把栈顶的俩个元素弹出来,用公钥检查这个签名是否正确,如果正确返回true,表示验证通过。否则的话执行出错。
实例
P2SH(Pay to Script Hash)
最后一种,也是最复杂的一种。这种形式的输出脚本给出的不是收款人的公钥的hash
。而是收款人提供的脚本的hash
,这个脚本叫RedeemScriptHash
。将来花这个钱的时候,输入脚本里要给出这个RedeemScript
的具体内容,同时还有给出能够让这个赎回脚本能够正常运行的签名。验证的时候分为两步,第一步验证输入脚本里给出的赎回脚本是不是跟输出脚本里给出的赎回脚本hash值匹配。如果不匹配的话,说明给出的赎回脚本是不对的。如果输入脚本匹配,则第二步会把赎回脚本内容当作操作指令执行一遍,看能不能全不顺利执行。如果俩步验证都通过,说明这个交易是合法的。
P2SH实现P2PK
这里的输入脚本给出签名,还有就是给出序列号的赎回脚本。而赎回脚本的内容就是给出公钥,然后用CHECKSIG
检查签名。下面的输出脚本用来验证输入脚本给出的赎回脚本是否正确。
执行情况
开始的时候也是把输入脚本和输出脚本拼接在一起。前两行来自输入脚本,后面三行来自输出脚本。
首先,把输入脚本的Sig
压入栈
然后把赎回脚本压入栈
接下来是弹出seriRS
,然后取hash
,再压入栈
然后把输出脚本给出的hash
压入栈
此时,栈顶就又俩个hash
,EQUAL
比较是否相等,如果相等,就从栈顶消失,如果不等就凉了。到这里第一阶段的验证就结束了,第一阶段验证是为了验证输入脚本中提供的脚本有没有被篡改。
记下来是第二阶段的验证,先把输入脚本提供的序列化的赎回脚本进行反序列化,在ppt上没有体现,这是每个节点自己完成的。然后依次执行这个脚本的内容,比如这里脚本内容,首先把PubKey
压入栈
然后通过CHECKSIG
来验证签名的正确性,验证通过则栈内有个true
为什么搞这么复杂?直接用P2PK
不就好了吗,为什么要写到RedeemScript
中来操作。确实对这个简单的例子来说,是比较复杂、犯不上。P2SH这个功能在最初比特币版本中是没有的。后来通过软分叉的形式加进去了,它的一个常见的应用场景就是对多重签名的支持。比特币系统中一个输出,可能要求多个签名才能把钱取出,比如某个公司账户可能要求任意三个签名才能转账。这样对私钥的泄露提供了一些安全的保护。比如说某个合伙人的私钥泄露了,那么问题也不大,因为还需要另外两人的签名才能生效。同时也为私钥的丢失提供一些冗余,5个人中即使有2个人把私钥忘掉了,剩下的三个人仍然可以把钱取出来。
MULTISIG多重签名
这个功能是通过CHECKMULTISIG
来实现的。输入脚本第一行相当于占位,因为CHECKMULTISIG
有个bug,执行的时候会从堆栈上多弹出一个元素,这个bug已经没法改了,因为比特币是个去中心化系统。要通过软件升级的方式代价是很大的,要改需要硬分叉。所以实际方案是在输入脚本里往栈上多压入一个无用元素。
执行情况
首先,前三行就是把一个无用元素压入栈,以及把俩个签名压入栈。这个时候输入脚本就执行完了。
接着把输出脚本里预值M压入栈
然后把三个公钥压入栈
然后把N
的值压入栈
最后执行CHECKMULTISIG
,看是不是有3个中的2个,如果正确,则验证通过
主要,这过程没有用到P2SH
,用到是比特币原生的CHECKMULTISIG
来实现的。作为输入脚本,这就输入过多。比如说购物网站,有5个合伙人,规则是5个中的3个就能完成交易,但是用户买东西,给购物网站转账要填写3个公钥,在填写5和3,给用户带来不方便的体验
所以就有了P2SH
,它的本质就是把复杂度从输出脚本转移到了输入脚本。
P2SH多重签名
现在这个输出脚本变得非常简单,只有三行,原来的复杂度被转移到了RedeemScript
中。输出脚本只需要给出这个RedeemScript
的hash
即可。这个赎回脚本里要给出这N
个公钥,还有N
和M
的值,这个赎回脚本是在输入脚本里提供的。也就是说是由收款人提供的。向前面购物网站的例子,网站只需要公布这个脚本的Hash
值即可,至于这个网站的规则是5个里选3个,还是3个里选2个,对用户来说是不可见的,也不关注的,用户没必要知道。
执行情况
第一行的FALSE
是给CHECKMULTISIG
的bug准备的无效的元素,然后依次把俩个签名压入栈
接下来是序列化的赎回脚本,压入栈,到这里输入脚本就执行完了。
下面是输出脚本,取Hash
然后把输出脚本提供的Hash压入栈
再验证俩个hash是否一致,到这里第一阶段就完成了。
下面开始第二阶段,开始执行脚本,依次把M压入栈、3个公钥压入栈、把N压入栈
最后检查多重签名的正确性,如果没问题,返回true
实例
Proof of Burn
这个脚本格式比较特殊,输出脚本开头是return
操作,后面可以跟任意内容。return
这个操作是无条件的返回错误。所以包含这个操作的脚本永远不可能通过验证,执行到return这个操作就会出错,然后执行就终止了,后面跟的内容没有机会执行。
PoB
Proof of Burn(PoB)是一种加密货币共识机制,它通过“燃烧”或永久性移除一定数量的代币来证明用户对网络的投入。这种方法主要用于确保网络参与者有足够的长期投资和参与网络维护的动机。
工作原理
在PoB机制中,矿工或验证者需将一部分加密货币发送到一个不可用的钱包地址(即“燃烧地址”),这些币将永久性地从流通中移除。燃烧币种的行为被网络记录,作为投入网络的证明。根据燃烧的币量和其他参数,矿工可以获得相应的挖矿权力或决策权。
优点
- 减少资源消耗:相比PoW,PoB不需要消耗大量电力和计算资源。
- 防止垄断:由于参与者需要销毁币来获取挖矿权力,这使得难以通过购买大量硬件或积累大量币种来控制网络。
- 鼓励长期投资:燃烧币种是一种长期的投资承诺,表明用户对网络的长期信心。
缺点
- 资本损失:燃烧币种意味着直接的经济损失,因为这些币将永久移出流通。
- 可持续性问题:长期来看,不断需要燃烧币种可能对币的供应产生压力,影响币的价值和稳定性。
- 难以吸引新用户:对新进入者而言,高昂的燃烧成本可能是一大障碍。
PoB是一种创新的共识机制,提供了一种不依赖昂贵设备或大量资金的方式来参与网络治理。然而,它的实际应用和普及度仍受限于其对参与者的经济损失要求。
为什么会设计这样的输出脚本?
这样的输出岂不是永远都花不出去了。不论输入脚本写到什么内容,执行到输出脚本,就报错了。那么这里的钱永远都花不出去。因为这种方式是证明比特币销毁的一种方法,为什么要销毁呢??一般有俩种应用场景,一种场景是一些小的币种,要求销毁一定数量的比特币才能得到这个币种,把这种小币种叫做Alternative Coin
或者AltCoin
,除了比特币以外其他币都可叫AltCoin
,比如有的币种要求销毁一个比特币,可以得到1000个小币,也就是说用这种方式证明你付出了一定的代价。另外一种场景是,往区块链里添加一些内容,通常认为区块链是不可篡改的账本。有人就利用这个特性,往里面添加一些需要永久保存的内容。比如digital commitment
,比如说把某项知识产权的内容取hash
之后,把hash
放在return
语句的后面,反正return
后面的都不会执行。这个不会占太大的空间,而且也没有泄露知识产权的内容,将来如果出现纠纷,到时候把知识产权的内容公布出去证明你在某个时间点已经知道这个知识了。
之前再了解coinbase
域时候,它也类似,还不用销毁比特币了随便写。为什么不用这个,因为coinbase
只有获得记账权的才有几个写。而return
方法是所有节点都可以写。发布区块需要记账权,而发布交易不需要记账权。任何用户都可以用这种方法,销毁很少一点比特币,比如0.0001个比特币,换取往区块链写入内容的机会。甚至有的根本没有消耗比特币,只是支付了交易费也行。
实例
这是一个coinbase tx
,这个交易有俩个输出。第一个输出是正常的P2PKH
,输出的金额就是得到的block reward
加上transaction fee
。第二个输出的金额是0,输出脚本就是Proof of Burn
格式,开头是return
,目的就是为了往区块链里写一些东西。
这是普通的转账交易,输出脚本也是已return
开头的。这个交易的输入是0.05个币,输出金额是0,说明输入金额全部用来支付交易费了。这个交易其实没有销毁任何比特币。只不过把输入里的比特币,作为交易费转给挖到矿的矿工了。
这种形式的好处是,矿工看到这种脚本的时候,知道它里面的输出永远不可能兑现。所以就没有必要保存到UTXO
里面,这样对全节点比较友好。
另外为了简介,都少加了OP
前缀,CHECKSIG
其实是OP_CHECKSIG
,DUP其实是OP_DUP
。比特币中的脚本语言是非常简单的,甚至连专门的名字都没有,就称为比特币脚本语言,Bitcoin Scripting language
。后面会接触的以太坊的智能合约语言就比这个复杂的多。比如比特币的脚本语言不支持循环,很多功能是实现不了的,这样的设计是有其用意的,不支持循环就不会死循环,不会停机。以太坊就有了汽油费的机制来防止死循环。另一方面,在密码学方面很强大, 检查多重签名用一条语句CHECKMULTISIG
就能完成,要比别的强的多。