目录
2.P2PKH(Pay to Public Key Hash)
如图1-1所示是一个在Blockchain.info上面查找到的交易信息。可以看到这个交易有一个输入和两个输出,两个输出都还没有被花出去。其输入的具体信息,输出的具体信息如图1-3所示。
输入脚本包含两个操作,分别将两个很长的数压入栈中,如图1-2所示。比特币使用的脚本语言是非常简单的,唯一能访问的就是一个堆栈,不像通用的编程语言(如C或C++等),有全局变量、局部变量和动态分配等问题。这里的脚本语言就是一个栈,所以是基于栈的编程语言。
输出脚本有两行,分别对应上面的两个输出,每个输出有自己单独的一段脚本,如图1-2所示。
这个交易还没有得到确认,所以还具有一定程度的回滚的可能性,Confirmations代表该交易被确认的个数,如图1-4所示。
- "result":{
- "txid":"921a.dd24",//交易的id
- "hash":"921a.dd24",//交易的哈希值
- "version":1,//使用的比特币协议版本号
- "size":226,//交易的大小
- "locktime":0,//设定交易的生效时间,0代表立即生效,如果非0代表经过几个区块后才允许上链
- "vin":[...],//交易的输入
- "vout":[...],//交易的输出
- "blockhash":"0000000000000000002c510d..5c0b",//交易所在区块的哈希值
- "confirmations":23,//目前已经有几个确认,包括自己及其后面有多少区块上链
- "time":1530846727,//交易产生的时间戳,即从交易产生到现在过来多少秒
- "blocktime":1530846727//该交易所在的区块的产生时间,即从交易所在的区块产生到现在过来多少秒
- }
- "vin":[{//交易的输入结构,是一个数组
- "txid":"c0cb...c57b",//该输入的币的“来源交易”的哈希值
- "vout":0,//该输入对应的“来源交易”的第几个输出,这实际上是一个索引值
- "scriptsig":{//输入脚本,这里是最简单的形式,只有签名,后面会用“input script”指代
- //如果有多个输入,每个输入都需要指明币的来源,并给出签名
- "asm":"3045...0018",
- "hex":"4830...0018"
- }
- }],
- "vout":[{
- "va1ue":0.22684000,//输出金额,即给对方转过去多少比特币
- "n":0,//序号,表示这是这个交易中的第几个输出,实际上也是一个索引
- "scriptPubKey":{//输出脚本,之后将以“output script”指代
- "asm":"DUP HASH160 628e...d/43 EQUALVERTFY CHECKSTG",//输出脚本的内容,包含一系列操作
- "hex":"76a9.88ac",
- "regsigs":1,//这个输出需要多少个签名才能兑现,有的输出需要多重签名,这里只需要一层签名
- "type":"pubkeyhash",//输出的类型,此处pubkeyhash是公钥的哈希
- "addresses":["19z8LJkNXLrTv2QK5jgTncJCGUEEfpQvSr"]//输出的地址
- }
- },{
- "va1ue":0.53756644,
- "n":1,
- "scriptPubKey":{
- "asm":"DUP HASH160 da7d...2cd2 EQUALVERIFY CHECKSIG",
- "hex":"76a9.88ac",
- "regsigs":1,
- "type":"pubkeyhash",
- "addresses":["1LVGTpdyeVLCLCDK2m9f7Pbh7zwhs7NYhX"]
- }
- }]
这是一个小型区块链,如图2-1所示,前面有一个A→B的一个转账交易,后面隔两个区块之后又有一个B→C的转账交易。B→C的这个交易中比特币的来源是A→B这个交易,所以我们可以看见如下图所示的B→C的交易中的txid和vout是指向A→B这个交易。
在早期的比特币系统中,B→C这个交易的输入脚本和A→B这个交易的输出脚本拼在一起执行的。后来出于安全因素的考虑,这两个脚本改为分别执行,首先执行输入脚本,如果没有出错,那么再执行输出脚本,如果能顺利执行,并且最后得到非零值(true),那么则说明这个交易就是合法的。如果一个交易有多个输入,其中每个输入脚本都要找到前面特定区块中所对应的输出脚本,匹配之后来进行验证。全部验证通过后,这个交易才是合法的。
(1)脚本内容
这是最简单的一种形式,输出脚本中直接给出收款人的公钥。第二行的CHECKSIG是检查签名的操作,在输入脚本里直接给出签名就可以,这个签名是用私钥对这个输入脚本所在交易的签名,这种形式是最简单的,因为Pubic Key是直接在输出脚本中给出的。
- input script:
- PUSHDATA(Sig)//签名,用私钥对输入脚本所在的整个交易的签名
- output script:
- PUSHDATA(PubKey)//收款人的公钥
- CHECKSIG//检查签名的操作
(2)脚本执行
把输入脚本和输出脚本连接起来的结果,实际代码中出于安全考虑,这两段代码是分别执行的。这里为了方便就把这两个脚本的代码拼接显示,逐条执行。
- PUSHDATA(Sig)//把输入脚本中提供的签名压入栈
- PUSHDATA(PubKey)//把输出脚本中提供的公钥压入栈
- CHECKSIG//把栈顶的两个元素弹出,用公钥检查这个签名是否正确
- //如果正确,则返回True,说明验证通过;否则,执行出错,说明该交易非法。
(3)实例
上面交易的输入脚本就是将签名压入栈,下面这个交易,是上面交易的币的来源,其输出有两行,第一行是将公钥压入栈,第二行是进行CHECKSIG,如下图3-1所示。
(1)脚本内容
P2PKH的输出脚本中没有给出收款人的公钥,给出的是公钥的哈希值。公钥是在输入脚本中给出的,输入脚本既要给出签名,还要给出公钥,输出脚本中其他还有一些为了验证签名正确性的操作。P2PKH是最常用的一种形式。
- input script:
- PUSHDATA(Sig)
- PUSHDATA(PubKey)
- output script:
- DUP
- HASH160
- PUSHDATA(PubKeyHash)
- EQUALVERIEY
- CHECKSIG
(2)脚本执行
- PUSHDATA(Sig)//将签名压入栈
- PUSHDATA(PubKey)//将公钥压入栈
- DUP//把栈顶元素,即公钥PubKey,复制一遍
- HASH160//弹出栈顶元素,取哈希,将得到的哈希值再压入栈
- PUSHDATA(PubKeyHash)//将输出脚本中提供的公钥哈希值压入栈
- EQUALVERIEY//弹出栈顶两个元素,做对比,防止有人冒名顶替,若它们从栈顶消失,则两个哈希值相等
- CHECKSIG//弹出栈顶两个元素,用公钥检查这个签名是否正确
- //如果正确,则返回True,说明验证通过,整个脚本彻底运行结束;否则,执行出错,说明该交易非法。
(3)实例
(1)脚本内容
这是最复杂的一种形式,这种形式下输出脚本给出的不是收款人的公钥的哈希,而是收款人提供的赎回脚本(Redeem Script)的哈希。将来要花这个输出脚本的BTC的时候,相应交易的输入脚本要给出赎回脚本的具体内容,同时还要给出让赎回脚本能正确运行所需要的签名。
- input script:
- ...
- PUSHDATA (Sig)//签名
- ...
- PUSHDATA(serialized redeemScript)//序列化赎回脚本
- output script:
- HASH160
- PUSHDATA(redeemScriptHash)//赎回脚本的哈希
- EQUAL
input script要给出一些数目不定的签名及一段序列化的Redeem Script(赎回脚本)。验证分如下两步:
①验证输入脚本里给出的RedeemScript是否与输出脚本中的给出的Redeem Script的哈希值匹配。如果不匹配,则说明输入脚本给出的Redeem Script是不对的。
②将赎回脚本的内容当作操作指令执行,看能否顺利执行,若两步验证都通过了,才能说明交易合法。
(2)用P2SH实现P2PK的功能
- redeemScript://赎回脚本
- PUSHDATA(PubKey)//公钥
- CHECKSIG//检查签名
- input script://输入脚本
- PUSHDATA(Sig)//签名
- PUSHDATA(serialized redeemScript)//序列化的赎回脚本
- output script://输出脚本,验证输入脚本中给出的赎回脚本是否正确
- HASH160
- PUSHDATA (redeemscriptHash)
- EQUAL
执行过程:
①第一阶段的验证
像之前一样把输入脚本和输出脚本拼接到一起,前两行来自输入脚本,后面三行来自于输出脚本。
- PUSHDATA(Sig)//将输入脚本的Signature压入栈
- PUSHDATA(serialized redeemScript)//将序列化的赎回脚本压入栈
- HASH160//取哈希值,得到赎回脚本的哈希值
- PUSHDATA (redeemscriptHash)//将输出脚本中赎回脚本的哈希值压入栈
- EQUAL//比较栈顶两个哈希值是否相等,如果相等,则这两个哈希值从栈顶消失
②第二阶段的验证
将第一阶段输入脚本中提供的序列化的赎回脚本进行反序列化,这个反序列操作,在代码部分没有展现出来,这个是每个结点需要自己完成的,然后再执行赎回脚本。
- PUSHDATA(PubKey)//将公钥压入栈
- CHECKSIG//验证输入脚本中给出的Signature的正确性,验证之后整个P2SH才算执行完
P2SH的常见应用场景就是对多重签名的支持。比特币系统中一个交易输出可能要求使用它的交易输入提供多个签名,才能把比特币取出来。比如某个公司可能要求5个合伙人中的任意三个提供签名,才能把公司的钱转走。这样设计不但为私钥的泄露提供了一定安全性保护,也为私钥的丢失提供了一定的容错性。这个操作是通过“CHECKMULTISIG”来实现的。
(1)脚本内容
- inputScript://提供N个公钥中任意M个合法的签名
- X//在输入脚本里往栈中添加一个没用的元素,抵消掉比特币其中的一个bug。
- PUSHDATA(Sig_1)//给出的签名的顺序需要与公钥顺序一致
- PUSHDATA(Sig_2)
- ...
- PUSHDATA(Sig_M)
- outputScript:
- M//阈值
- PUSHDATA(pubkey_1)
- PUSHDATA(pubkey_2)
- ...
- PUSHDATA(pubkey_N)
- N//公钥的个数
- CHECKMULTISIG
(2)脚本执行
- FALSE//压入栈的多余元素
- PUSHDATA(Sig_1)//将两个签名压入栈
- PUSHDATA(Sig_2)
- 2//阈值M压入栈
- PUSHDATA(pubkey_1)//将三个公钥分别压入栈
- PUSHDATA(pubkey_2)
- PUSHDATA(pubkey_3)
- 3//将公钥数目N压入栈
- CHECKMULTISIG//对比看堆栈中是否包含N个签名中的M个,这里是3个签名中的2个
- //如果是,则验证通过
这是最早的多重签名,并没有用到P2SH,就是用比特币脚本中原生的CEHCKMULTISIG实现的。这样在实际使用时有些不方便的地方,例如某个电商平台开通了比特币支付渠道,按要求需要有5个合伙人中3个人的签名才能把比特币转走。但这样做之后,用户在使用比特币支付的时候,生成的转账交易里也需要给出5个合伙人的公钥,同时还要给出N和M的值。而这些公钥,以及N和M的值就要电商平台公布给用户,而且不同的电商平台规则也不一样,这就让用户生成转账交易变得不方便。这就给用户生成转账交易带来了不便,因为这些复杂性都暴露给用户了。
(1)脚本内容
与之前的的多重签名相比,P2SH的本质是将复杂性从输出脚本转移到了赎回脚本中,输出脚本变得十分简单,只需要给出赎回脚本的哈希值就行了。N个公钥以及N、M的值都在赎回脚本中给出,而赎回脚本由输入脚本提供(也就是说由收款人提供),这样也就大大降低了用户的工作量。
- inputScript://提供N个公钥中任意M个合法的签名
- X//在输入脚本里往栈中添加一个没用的元素,抵消掉比特币其中的一个bug。
- PUSHDATA(Sig_1)//给出的签名的顺序需要与公钥顺序一致
- PUSHDATA(Sig_2)
- ...
- PUSHDATA(Sig_M)
- PUSHDATA(serialized RedeemScript)
- outputScript:
- HASH160
- PUSHDATA(RedeemScriptHASH)
- EQUAL
- redeemScript:
- M//阈值
- PUSHDATA(pubkey_1)
- PUSHDATA(pubkey_2)
- ...
- PUSHDATA(pubkey_N)
- N//公钥的个数
- CHECKMULTISIG
从用户的角度来看,采用这种P2SH的支付方式,和P2PKH支付方式没有多大区别,只不过输出脚本中的是赎回脚本的哈希值而不是公钥的哈希值,当然,输出脚本的写法上也有一些细微的区别。输入脚本就是电商要把这笔比特币转出去时候用的,这种方式下输入脚本要包含让赎回脚本验证通过的M个签名,以及赎回脚本的序列化版本。如果电商平台将来改变了采用的多重签名规则,就只需要改变一下赎回脚本的内容和输入脚本中的内容,然后把新的赎回脚本的哈希值公布出去就可以了。对用户而言也只是付款时候输出脚本中要包含的哈希值发生了变化,其他的变化是用户不需要知道的内容。
(2)脚本执行
①第一阶段验证
- FALSE//多余的元素压入栈
- PUSHDATA(Sig_1)//将签名Sig_1压入栈
- PUSHDATA(Sig_2)//将签名Sig_2压入栈
- PUSHDATA(serialized RedeemScript)//将序列化的赎回脚本压入栈
- HASH160//对序列化的赎回脚本取哈希,然后将哈希值压入栈
- PUSHDATA(RedeemScriptHASH)//将输出脚本中提供的赎回脚本的哈希值压入栈
- EQUAL//将栈顶的两个哈希值进行对比,若相等,则第一阶段验证完成
②第二阶段验证
- 2//阈值,压入栈
- PUSHDATA(pubkey_1)//将pubkey_1公钥压入栈
- PUSHDATA(pubkey_2)//将pubkey_2公钥压入栈
- PUSHDATA(pubkey_3)//将pubkey_3公钥压入栈
- 3//公钥的个数,压入栈
- CHECKMULTISIG//检查多重签名的正确性
这是一种特殊的输出脚本,执行到RETURN语句就会出错,然后验证就会终止,后面的语句完全没有机会执行,包含这个操作的语句永远不可能通过验证。这个脚本常常用来销毁比特币。
为什么要销毁比特币? ①一些小的加密货币(AltCoin:Alternative Coin),除了比特币之外的加密货币都可以被认为是小币种,他们要求销毁一定数量的比特币可以得到一定数量的这种币。这时Proof of Burn就可以证明自己销毁了这些比特币。②往区块链里写入一些内容。因为区块链是不可篡改的账本,有人就利用这个特性向其中写入一些需要永久保存的内容。比如,第一节课学的Digital Commitment,即需要证明自己在某一时间知道某些内容。例如某些知识产权保护,可以将知识产权取哈希之后,将哈希值放在这种输出脚本的RETURN语句的后面。反正哈希值很小,而且哈希值没有泄露原来的内容,将来出现纠纷时,再将原来的内容公布出去,大家在区块链上找到这个交易的输出脚本里的哈希值,就可以证明自己在某个时间点已经掌握了这些知识了。
对于上面说的②的应用场景,回想在前面学习到铸币交易时,铸币交易的CoinBase域也可以随便写什么内容,为什么不在那里写呢,还不需要销毁比特币?这种方法很难,必须要获得记账权,而且是在CoinBase域设定好内容的情况下,去获得记账权。根本来说,是因为发布交易不需要有记账权,但发布区块需要取得记账权。任何用户都可以用Proof of Burn的方法,销毁极少量的比特币,换取向比特币系统的区块链中写入一些内容的机会。
(1)销毁了比特币的形式:
(2)没有销毁比特币,仅仅支付了交易费,也可以向区块链中写入内容:
比特币系统中使用的脚本语言很简单,甚至不支持循环,这样设计也有其用意,不支持循环也就不会有死循环。后面学的以太坊的脚本语言就是图灵完备的,这样就靠其它机制来防止进入死循环等。比特币的脚本语言针对比特币应用场景做了很好的优化,在密码学方面的功能尤其强大,如检查多重签名时的CHECKMULTISIG操作一条就能实现,这是其强大之处。