1.比较操作码在前面我们了解了if脚本怎么写接下来我们来加上条件表达式判断比如12一个完整的条件判断执行脚本如下// 1. 构造脚本 CScript script; script 1 2 OP_GREATERTHAN; script OP_IF 3 5 OP_ADD;//如果为真则执行35 script OP_ELSE 1 9 OP_ADD;//如果为假则执行19 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值数组容器请看定义vectorbool vfExec;它是用来记录if分支可不可执行。我们知道解析器是一个操作码一个操作码执行的每循环一次执行一个操作码while (pc pend)对应这句switch (opcode)那么当执行到OP_IF操作码时比如下面这样OP_IF 3 5 OP_ADD;//如果为真则执行35 OP_ELSE 1 9 OP_ADD;//如果为假则执行19 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取出来然后bnbn1bn2相加得到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 theres 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, vectorvectorunsigned char * pvStackRet)而这个EvalScript并不是一个孤立的函数它需要外部调用而在外部调用它会传tx参数。这个tx参数是用来干什么的呢其中就是带有交易数据然后可以动态生成哈希值。这样就得到了签名数据来源。CheckSig是使用了txTo参数。我们来回想一下签名是怎么生成的这笔tx又是怎么个验证流程。首先构建的时候这笔tx。它会根据情况去掉这笔tx下的一些数据然后生成哈希值最终是对这个哈希值进行签名。那么验证的时候它不是直接传这个哈希值来验证的而是验证这笔tx时根据规则重新生成一遍哈希值接着再验证。所以你在脚本里看不到哈希值。12.CheckSig我们来看这个函数的代码bool CheckSig(vectorunsigned char vchSig, vectorunsigned 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操作码执行失败返回falseverifySignature验证不通过。