二十八.签名与脚本(3)--脚本解析
1.比较操作码
在前面我们了解了if脚本怎么写,接下来我们来加上条件表达式判断,比如1>2,一个完整的条件判断执行脚本,如下:
// 1. 构造脚本 CScript script; script << 1 << 2 << OP_GREATERTHAN; script << OP_IF << 3 << 5 << OP_ADD;//如果为真则执行3+5 script << OP_ELSE << 1 << 9 << OP_ADD;//如果为假则执行1+9 script << OP_ENDIF;判断1是否比2大,利用了OP_GREATERTHAN操作码,就是比较栈中两个数,然后把结果存在栈中,跟OP_ADD同样的原理,>符号对应OP_GREATERTHAN(greater than),+符号对应OP_ADD,没什么特别的,还有其它的比较操作码,如<,==可自行查询对应的OP_码。
2.count
接下来我们再来理解if语句在脚本上,是怎么解析的,就是根据真假怎么执行语句功能。
我们回到EvalScript里,先来看这句:
bool fExec = !count(vfExec.begin(), vfExec.end(), false);count函数是标准库算,是用来统计容器中,某个值出现的次数,第一个参数为这个容器起始迭代器,第二个参数为结束迭代器,第三个参数为要查找的值。
那么这句的代码的意思是,如果vfExec里只要有一个元素为假,那么fExec就为假。相反,只有当vfExec里所有元素为真是,那么fExec才为真。
3.vfExec
那这里的vfExec是干什么的呢,它是一个bool值数组容器,请看定义:
vector<bool> vfExec;它是用来记录if分支可不可执行。
我们知道,解析器是一个操作码一个操作码执行的,每循环一次执行一个操作码:
while (pc < pend)对应这句:
switch (opcode)那么当执行到OP_IF操作码时,比如下面这样:
OP_IF 3 << 5 << OP_ADD;//如果为真则执行3+5 OP_ELSE 1 << 9 << OP_ADD;//如果为假则执行1+9 OP_ENDIF;此时vfExec里还没有假,所以fExec为真
else if (fExec || (OP_IF <= opcode && opcode <= OP_ENDIF)) switch (opcode) {4.OP_IF
所以它会执行OP_IF操作码,switch进入OP_IF操作码处理逻辑,如下:
case OP_IF: case OP_NOTIF: case OP_VERIF: case OP_VERNOTIF: { // <expression> if [statements] [else [statements]] endif bool fValue = false; if (fExec) { if (stack.size() < 1) return false; valtype& vch = stacktop(-1); if (opcode == OP_VERIF || opcode == OP_VERNOTIF) fValue = (CBigNum(VERSION) >= CBigNum(vch)); else fValue = CastToBool(vch); if (opcode == OP_NOTIF || opcode == OP_VERNOTIF) fValue = !fValue; stack.pop_back(); } vfExec.push_back(fValue); } break;以上,当第一次执行OP_IF后,实际执行的代码:
bool fValue = false; // 先默认设为 false if (fExec) // ← 这里 fExec == true,进入 { if (stack.size() < 1) // 检查栈里有没有条件值 return false; valtype& vch = stacktop(-1); // 取出栈顶元素(这就是 if 的条件) //定义为#define stacktop(i) (stack.at(stack.size()+(i))) 可通过负数控制取倒数第几个 // 因为 opcode 是 OP_IF,不是 VERIF 类 fValue = CastToBool(vch); // 把栈顶数据转为 bool(最关键一步) // 因为不是 OP_NOTIF,也不是 OP_VERNOTIF,所以不取反 // fValue = !fValue; 这一行不会执行 stack.pop_back(); // 重要!把条件值从栈中弹出 } // 注意:这一行在 if(fExec) 外面!无论 fExec 是真是假都会执行 vfExec.push_back(fValue); // 把计算出的 true/false 压入 vfExec 向量也就是说,当执行OP_IF后,处理逻辑就是将栈顶的值提取出来,然后添加进vfExec里面。
当然其它操作码也是干同样的事,只是根据逻辑取不同的真和假,比如OP_NOTIF,还要给值取反。
我们假设这个添加进去的值为false,那么第二次循环后,fExec为假,后续所有的操作码都不会被执行。直到遇到以下操作码:
|| (OP_IF <= opcode && opcode <= OP_ENDIF))也就是说范围在OP_IF---OP_ENDIF内的操作码,我们看ENUM定义,就能知道是哪些,如下:
// control OP_NOP, OP_VER, OP_IF, //从这里开始 OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF,//到这里结束 OP_VERIFY, OP_RETURN,那么当遇到OP_ELSE时,它就会开始执行OP_ELSE操作码,或者OP_ENDIF操作码。
另注意这里OP_IF可以没有OP_ELSE,但必须要有OP_ENDIF结尾。
5.OP_ELSE
我们来看一下OP_ELSE处理逻辑:
case OP_ELSE: { if (vfExec.empty()) return false; vfExec.back() = !vfExec.back(); } break;将vfExec里的值,取反操作,那么接下来的操作码又可以开始执行了,抽象的理解就是,也即如果条件为假,则执行OP_ELSE下的语句。
如果为真,那么取反后,后面的语句又不会被执行了。
然后在OP_ENDIF操作码的逻辑处理里,进行收尾工作。
6.OP_ENDIF
当执行到OP_ENDIF,说明这一个IF语句已经执行完成,所以会删掉这个对应的vfExec bool值,处理代码如下:
case OP_ENDIF: { if (vfExec.empty()) return false; vfExec.pop_back(); }这样后面的语句又恢复到的开始的时候(vfExec状态),从新开始逻辑处理。
当然如果有嵌套OP_IF,逻辑是一样的,pop_back是一层一层弹出,并不会出错。
7.OP_ADD
最后,关于操作码解析,我们来看一下OP_ADD的实现,其他的操作码逻辑都差不多,有需要可以自行看源码研究:
case OP_ADD: case OP_SUB: case OP_MUL: case OP_DIV: case OP_MOD: case OP_LSHIFT: case OP_RSHIFT: case OP_BOOLAND: case OP_BOOLOR: case OP_NUMEQUAL: case OP_NUMEQUALVERIFY: case OP_NUMNOTEQUAL: case OP_LESSTHAN: case OP_GREATERTHAN: case OP_LESSTHANOREQUAL: case OP_GREATERTHANOREQUAL: case OP_MIN: case OP_MAX: { // (x1 x2 -- out) if (stack.size() < 2) return false; CBigNum bn1(stacktop(-2)); CBigNum bn2(stacktop(-1)); CBigNum bn; switch (opcode) { case OP_ADD: bn = bn1 + bn2; break;bn1和bn2就是栈里倒数第1和倒数第2的两个数,也就是栈顶和栈顶之前的那个数,通过stacktop取出来,然后bn=bn1+bn2相加得到bn.
接着在上一级case里,处理bn,如下:
stack.pop_back(); stack.pop_back(); stack.push_back(bn.getvch());删掉栈中的两个数,然后将bn写进栈里。这就是OP_ADD的解析处理流程。
在深入了解脚本后,我们就可以来看比特币中的签名脚本了,还记得这部分代码吗:
CScript scriptPubKey; scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;这里CTxOut里的锁定脚本:
class CTxOut { public: int64 nValue; // 金额(单位:聪) CScript scriptPubKey; // ← 这里就是你看到的 scriptPubKey // ... 其他成员和函数 };需要CTxIn里的对应的解锁脚本,把它们凑成一个完整的脚本,然后执行需要得到结果为真才合法。我们现在来分析一下,理解这个脚本完整的执行流程。
首先是构建:
scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;全是操作码,只有一个hash160为变量。
该代码是在下面这个函数里(ui.cpp):(注意这里有两种vout,引用的vout.scriptPubKey和转账的接收者vout.scriptPubKey,后者是给未来引用的,前者是和当下构建的tx.scriptSig拼接,是自己的vout)
void CSendDialog::OnButtonSend(wxCommandEvent& event)那么这个hash160就是收款方公钥的hash160格式,你可以把它当作一种格式的账户。
8.OP_DUB
我们先来看OP_DUB操作码,这个是复制栈顶元素操作,但我们看到,scriptPubKey里前面并没有操作,栈顶没有状态,那它在复制什么?唯一的可能是,拼接脚本时,scriptPubKey为后段脚本,解锁脚本在前,我查看了相关代码,验证了我的猜想,如下:
return EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType);scriptSig在前, 好,那么这里我们来看一下scriptSig脚本里的内容是什么。
我们需要找到它的构造代码,然后再来分析。
9.Solver
签名最核心的地方,是在Solver函数构造的,我们可以在这个函数里看到关键的代码:
if (item.first == OP_PUBKEY) { // Sign ... scriptSigRet << vchSig; } } else if (item.first == OP_PUBKEYHASH) { // Sign and give pubkey ... scriptSigRet << vchSig << vchPubKey; }当是P2PKH的公钥后(账户格式),构造时txin.scriptSig他会:
scriptSigRet << vchSig << vchPubKey;在末尾补上公钥vchPubKey,这样就和锁定脚本scriptPubKey的OP_DUP对上了,复制的就是vchPubKey。
但是如果它是p2pk格式,即OP_PUBKEY,则只有一个签名,没有写入公钥,那么此时和锁定脚本拼接上,那会出错?OP_DUP复制的是签名, 跟后面的脚本对不上了。因为后面要比较账号,你签名肯定不等于账号。
这是为什么呢?
其实是不同的账号格式有不同的脚本,OP_DUP这个锁定脚本只用于P2PKH格式。
如果是p2pk格式,那么锁定脚本大概是这样(无OP_DUP行为):
scriptPubKey << pubkey << OP_CHECKSIG;当然这种格式用的少,所以可以忽略,比如ui界面就不支持,只能用P2PKH,所以那里的发送处理函数(OnButtonSend),并没有增加格式判断,从而构建P2PK格式的脚本。
10.验证脚本
好,我们了解了P2PKH的锁定和解锁脚本,那么把它们拼在一起,完整的执行脚本就是:
script << vchSig << vchPubKey; script << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG;它首先会验证你提供的公钥是否跟引用的out公钥相等。这个过程就是OP_DUP,复制一份公钥,这里复制是用来比较的,比较完会弹出,消耗掉公钥,所以要复制一份。因为公钥在后面OP_CHECKSIG验证签名还会用到一次。
OP_DUP复制完后,就调用OP_HASH160,将公钥转换成hash160格式,然后再压入我们从out获得的hash160,接着调用OP_EQUALVERIFY操作码进行对比两个has160是否一致。
如果一致,接着就调用OP_CHECKSIG来验证,此时的脚本已经变成这样:
vchSig << vchPubKey<< OP_CHECKSIG;我们可以看到,生成的签名数据有了,就是vchSig,这个是基于哈希生成的结果,公钥也有了vchPubKey,但还差一个东西,验证签名三要素,你是基于谁签名的呢?就是哈希值在哪里?
11.OP_CHECKSIG
要搞懂细节,我们需要研究OP_CHECKSIG操作码,代码如下:
case OP_CHECKSIG: case OP_CHECKSIGVERIFY: { // (sig pubkey -- bool) if (stack.size() < 2) return false; valtype& vchSig = stacktop(-2); valtype& vchPubKey = stacktop(-1); ////// debug print //PrintHex(vchSig.begin(), vchSig.end(), "sig: %s\n"); //PrintHex(vchPubKey.begin(), vchPubKey.end(), "pubkey: %s\n"); // Subset of script starting at the most recent codeseparator CScript scriptCode(pbegincodehash, pend); // Drop the signature, since there's no way for a signature to sign itself scriptCode.FindAndDelete(CScript(vchSig)); bool fSuccess = CheckSig(vchSig, vchPubKey, scriptCode, txTo, nIn, nHashType); stack.pop_back(); stack.pop_back(); stack.push_back(fSuccess ? vchTrue : vchFalse); if (opcode == OP_CHECKSIGVERIFY) { if (fSuccess) stack.pop_back(); else pc = pend; } } break;最终里面是调用CheckSig函数来进行验证的,我们看它后面几个参数。
是传了CTransaction类型的,要搞清楚脚本为什么没有交易数据的哈希值,缺少了签名的原始数据。
我们需要综合的看待,脚本是在EvalScript函数里执行的。
bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType, vector<vector<unsigned char> >* pvStackRet)而这个EvalScript并不是一个孤立的函数,它需要外部调用,而在外部调用它会传tx参数。
这个tx参数是用来干什么的呢?其中就是带有交易数据,然后可以动态生成哈希值。这样就得到了签名数据来源。CheckSig是使用了txTo参数。
我们来回想一下,签名是怎么生成的,这笔tx又是怎么个验证流程。
首先构建的时候,这笔tx。它会根据情况,去掉这笔tx下的一些数据,然后生成哈希值,最终是对这个哈希值进行签名。那么验证的时候,它不是直接传这个哈希值来验证的,而是验证这笔tx时,根据规则重新生成一遍哈希值,接着再验证。所以你在脚本里看不到哈希值。
12.CheckSig
我们来看这个函数的代码:
bool CheckSig(vector<unsigned char> vchSig, vector<unsigned char> vchPubKey, CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType) { CKey key; if (!key.SetPubKey(vchPubKey)) return false; // Hash type is one byte tacked on to the end of the signature if (vchSig.empty()) return false; if (nHashType == 0) nHashType = vchSig.back(); else if (nHashType != vchSig.back()) return false; vchSig.pop_back(); if (key.Verify(SignatureHash(scriptCode, txTo, nIn, nHashType), vchSig)) return true; return false; }关键一句:
if (key.Verify(SignatureHash(scriptCode, txTo, nIn, nHashType), vchSig)) return true;在里面同样的调用了SignatureHash函数,这个函数,我们在构建签名时SignSignature函数中,也同样调用了。可见这个哈希值是用到时生成的。
12.验证流程
整个过程明白了,我们来看之前的这部分代码:
bool VerifySignature(const CTransaction& txFrom, const CTransaction& txTo, unsigned int nIn, int nHashType) { assert(nIn < txTo.vin.size()); const CTxIn& txin = txTo.vin[nIn]; if (txin.prevout.n >= txFrom.vout.size()) return false; const CTxOut& txout = txFrom.vout[txin.prevout.n]; if (txin.prevout.hash != txFrom.GetHash()) return false; return EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn, nHashType); }是调用VerifySignature来验证的,里面调用了EvalScript函数。它并不是直接给出验证的三个数据验证。而是有需要验证的该笔tx实体参与,这样就补全了缺失的数据。这样逻辑就完美好。
执行EvalScript如果OP_CHECKSIG操作码执行失败,返回false,verifySignature验证不通过。
