区块链学习笔记——一些交易脚本(P2PK、P2PKH、P2MS、P2SH)及作业回顾

作者:JJT_with_hair

时间:2020年11月18日 12:41:09

来源:https://blog.csdn.net/weixin_44190459/article/details/109771045

写在前面


这篇博客主要总结一下之前做过的区块链作业中的一些有趣的东西。

实验环境搭建


按照老师的要求以及助教给的一些问题的解决方案。把python从3.8装回3.7再装回3.6,好像都没什么用。在pycharm运行代码总是说缺少依赖,然而明明已经装过了,点击它给的解决方案安装依赖,然后安装失败。于是我灵机一动,把之前装过的都卸掉了,再点那个安装,还是失败,于是我打开里面的虚拟终端通过命令行来安装。终于不提示缺少依赖。然而运行的时候,提示我的python-bitcoinlib库有问题,点进去查看源码依然毫无头绪。然后打开了虚拟机里的Ubuntu,安装依赖后可以跑代码。于是我的环境终于有了。

领取测试币


进到比特币测试网络领取比特币,在找完公交车或者自行车以及斑马线之后总是卡住不同,于是挂vpn再访问终于施舍给了我点币子。

比特币脚本(暂时了解的)


比特币脚本语言是一种基于栈的脚本语言(每个命令只执行一次),不是图灵完备的。比特币脚本包含两个部分,解锁脚本+锁定脚本,连在一起才是完整的,运行正确之后在栈中返回一个Ture。

公钥支付 P2PK (Pay to Publish Key)

锁定脚本

<pubkey>         #公钥
OP_CKECKSIG      #验证签名的脚本语言

解锁脚本

<Sig>            #签名

OP_CKECKSIG的部分函数定义,大致看起来就是利用pubkey生成一个数字签名,再与输入的签名比较,最后如果是ok就会在栈里留下一个Ture

def _CheckSig(sig, pubkey, script, txTo, inIdx, err_raiser):
    key = bitcoin.core.key.CECKey()
    key.set_pubkey(pubkey)

    if len(sig) == 0:
        return False
    hashtype = _bord(sig[-1])
    sig = sig[:-1]
    (h, err) = RawSignatureHash(script, txTo, inIdx, hashtype)
    return key.verify(h, sig)
-------------------------------------------------------
elif sop == OP_CHECKSIG or sop == OP_CHECKSIGVERIFY:
                check_args(2)
                vchPubKey = stack[-1]
                vchSig = stack[-2]
                tmpScript = CScript(scriptIn[pbegincodehash:])
                tmpScript = FindAndDelete(tmpScript, CScript([vchSig]))

                ok = _CheckSig(vchSig, vchPubKey, tmpScript, txTo, inIdx,
                               err_raiser)
                if not ok and sop == OP_CHECKSIGVERIFY:
                    err_raiser(VerifyOpFailedError, sop)

                else:
                    stack.pop()
                    stack.pop()

                    if ok:
                        if sop != OP_CHECKSIGVERIFY:
                            stack.append(b"\x01")
                    else:
                        # FIXME: this is incorrect, but not caught by existing
                        # test cases
                        stack.append(b"\x00")

完整的脚本

<Sig>
----------
<pubkey>         
OP_CKECKSIG

执行过程

NULL|初始状态,栈为空
<Sig>|<Sig>压栈
<Sig><pubkey>|<pubkey>压栈
Ture|执行OP_CHECKSIG,成功会把前两个东西pop掉,压入一个Ture

P2PKH(Pay To Public Key Hash)

刚开始不知道公钥哈希值是什么,原来就是我们的地址。私钥通过SHA256得到公钥,公钥经过RIPEMD160得到公钥哈希,公钥哈希经过Base58编码就是地址,某种意义上,地址就是公钥哈希


锁定脚本

OP_DUP
OP_HASH160
<address>
OP_EQUALVERIFY
OP_CHECKSIG

解锁脚本

<sig>
<pubkey>

完整脚本

<sig>
<pubkey>
----------
OP_DUP
OP_HASH160
<address>
OP_EQUALVERIFY   #检验两个值是否相等,不会返回值,只会删去栈顶两个元素
OP_CHECKSIG

 

执行过程

NULL	| 初始状态栈为空
<sig>	| sig压栈
<sig><pubKey>	| pubKey压栈
<sig><pubKey><pubKey>	| OP_DUP复制栈顶元素并将其压栈
<sig><pubKey><pubKeyHash>	| OP_HASH160计算公钥哈希值并用其替换栈顶
<sig><pubKey><pubKeyHash><pubKeyHash?>	| 已知的address压栈,即不知道
<sig><pubKey>	| 比较address和pubKeyHash,相等后抛弃栈顶开始的两位,不相等就终止
true	| 用CHECKSIG验证签名有效,有效则true,无效则false

多重签名P2MS(Multiple Signatures)


对于一个M-N的交易(M是需要认证的人数,N是总人数,即只要N中M个人的签名),锁定脚本为:

OP_M
<PublicKey 1>
<PublicKey 2> 
... 
<PublicKey N> 
OP_N 
OP_CHECKMULTISIG

解锁脚本

OP_0            # OP_CHECKMULTISIG的一个bug,下面会解释
<Sig1>
<Sig2>
...
<SigM>          # N个人中随意M个人的签名

 

完整脚本

OP_0            
<Sig1>
<Sig2>
...
<SigM>
----------
OP_M
<PublicKey 1>
<PublicKey 2> 
... 
<PublicKey N> 
OP_N 
OP_CHECKMULTISIG

 

执行过程

NULL |初始状态,栈为空
0    |OP_0将0压栈
0<Sig1><Sig2>···<SigM>|M个签名入栈
0<Sig1><Sig2>···<SigM>M|OP_M把M压栈
0<Sig1><Sig2>···<SigM>M<PublicKey 1><PublicKey 2>···<PublicKey N>  |N个PublicKey入栈
0<Sig1><Sig2>···<SigM>M<PublicKey 1><PublicKey 2>···<PublicKey N>N|OP_N把N压栈
True |OP_CHECKMULTISIG进行验证,成功则返回一个ture

OP_0是OP_CHECKMULTISIG的一个bug,不过由于历史遗留问题,debug成本过高,所以就遗留下来了,没什么具体含义,可以当成一个小彩蛋。OP_CHECKMULTISIG所做的具体操作

弹出一个N,得到公钥数量
弹出N个公钥的值
弹出一个M,得到签名数量
弹出M个签名
得到了所有数据,计算脚本是否有效

在弹出M个签名之后,会再pop一次,如果没有OP_0,存在,这时候栈已经空了,pop会出错,脚本没法运行,OP_0就是为了这个bug而生的,弹出的OP_0不会对脚本运行结果产生影响,只是让它能够运行,所以压入什么数应该都可以

脚本哈希支付P2SH(Pay to Script Hash)

一开始以为这个和Multiple Signatures是一样的东西,后来看了很多博客才知道是可以通过这种方法实现P2MS,P2SH是2012年提出的,个人理解这个脚本是为了防止Script过于长而导致交易失败,通过HASH160得到脚本的哈希值,这样就能解决长度问题


P2SH中的脚本包括三部分,赎回脚本(redeem Script)、锁定脚本(locking Script)、解锁脚本(unlocking Script)


赎回脚本(redeem Script)类似于之前脚本里的锁定脚本(locking Script)内容是一致的
锁定脚本(locking Script)形式是固定的:

OP_HASH160
<redeem Script hash>   #redeem Script的哈希
OP_EQUAL

解锁脚本(unlocking Script)与赎回脚本相关

···
<Sig>
···       #需要的签名以及其他内容
<serialized redeem Script> #序列化的赎回脚本,作为数据而不作为脚本语言压栈

脚本的执行过程

首先就是解锁的脚本压栈
此时栈顶就是<serialized redeem Script>
然后锁定脚本依次压栈
先得到赎回栈顶的哈希值
再与锁定脚本中的哈希值对比两者不匹配验证就失败
成功则序列化脚本会被反序列化再与栈内的剩余内容(也就是解锁脚本中剩下的所有<Sig>或者其他内容)构成完整的脚本

下面以实现P2PKH与P2MS两种脚本为例写两个P2SH脚本


P2PKH

赎回脚本(redeem Script)

OP_DUP
OP_HASH160
<address>
OP_EQUALVERIFY
OP_CHECKSIG

锁定脚本(locking Script)

OP_HASH160
<redeem Script hash>   #redeem Script的哈希
OP_EQUAL

解锁脚本(unlocking Script)

<Sig>
<Pubkey>
<serialized redeem Script>

完整脚本

<Sig>
<Pubkey>
<serialized redeem Script>
----------
OP_HASH160
<redeem Script hash>   
OP_EQUAL

P2MS

赎回脚本(redeem Script)

OP_M
<PublicKey 1>
<PublicKey 2> 
... 
<PublicKey N> 
OP_N 
OP_CHECKMULTISIG

锁定脚本(locking Script)

OP_HASH160
<redeem Script hash>   #redeem Script的哈希
OP_EQUAL

解锁脚本(unlocking Script)

OP_0            
<Sig1>
<Sig2>
...
<SigM>          # N个人中随意M个人的签名
<serialized redeem Script>

完整脚本

OP_0            
<Sig1>
<Sig2>
...
<SigM>          
<serialized redeem Script>
----------
OP_HASH160
<redeem Script hash>  
OP_EQUAL

作业中的unknown脚本(P2PK+P2MS)

作业要求

  • 生成一个涉及四方的多签名交易,这样交易可以由第一方(银行)与另外三方(客户)中的任何一方(客户)共同赎回,而不仅仅只是客户或银行。对于这个问题,你可以假设是银行的角色,这样银行的私钥就是你的私钥,而银行的公钥就是你的公钥。使用keygen.py生成客户密钥并将它们粘贴到ex2a.py中。
  • 赎回事务并确保scriptPubKey尽可能小。可以使用任何合法的签名组合来赎回交易,但要确保所有组合都有效

解题思路

该交易中一共有四位参与者,银行与三位客户,需要银行与其他任何一个用户的签名才能赎回交易,如果只用多重签名,那么不需要银行的签名也能赎回,所以我们要把实验分两步,一步是验证银行签名,另外一步是验证客户签名。所以可以用P2PK+P2MS构成的脚本

脚本实现

其实上述两个步骤没有先后顺序,银行和客户身份先验证后验证都无所谓,反正逃不过验证,只不过用的脚本会不一样

先银行后客户

锁定脚本

<pubkey bank>     #银行公钥
OP_CHECKSIGVERIFY #使用OP_CHECKSIGVERIFY而不用OP_CHECKSIG是因为它不会有在栈里压数剧,后面脚本执行不会出错
OP_1
<pubkey 1>
<pubkey 2>
<pubkey 3>        #三个客户的公钥
OP_3
OP_CHECKMULTISIG

解锁脚本

OP_0             
<sig cust>
<sig bank>       #注意顺序,因为先验证银行,所以银行的pubkey要在栈顶

完整脚本

OP_0
<sig cust>
<sig bank> 
----------
<pubkey bank>     
OP_CHECKSIGVERIFY
OP_1
<pubkey 1>
<pubkey 2>
<pubkey 3>        
OP_3
OP_CHECKMULTISIG

执行过程

NULL	| 起始状态
0           | OP_0压栈
0<sig cust>	| <sig cust>压栈
0<sig cust><sig bank>	| <sig bank>压栈
0<sig cust><sig bank><pubkey bank>	| pubkey bank压栈
0<sig cust>	| CHECKSIGVERIFY验证<sig bank>和<pubkey bank>是否匹配并将它们pop掉,匹配则继续,不匹配直接就验证不成功
0<sig cust> 1	| OP_1压栈,确定需要检查的参数
0<sig cust> 1 <pubkey 1><pubkey 2><pubkey 3>	| 3个pubkey压栈
0<sig cust> 1 <pubkey 1><pubkey 2><pubkey 3> 3  | OP_3确定3个参数作为检查的范围
true	| OP_CHECKMULTISIG检查多重签名,成功返回一个ture,否则验证失败

先客户后银行

原理相同只给出完整脚本(注意脚本指令变了)

<sig bank> 
OP_0
<sig cust>
----------
OP_1
<pubkey 1>
<pubkey 2>
<pubkey 3>        
OP_3
OP_CHECKMULTISIGVERIFY
<pubkey bank>     
OP_CHECKSIG

相关文章:

比特币布道者

比特币的坚定信仰者!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注