二十六.签名与脚本(1)--脚本介绍

二十六.签名与脚本(1)--脚本介绍 1.区块链脚本介绍在之前的章节中我们了解了签名与验证相关但是btc的交易数据签名和验证不是单纯的还有脚本深度参与其中。我们从开始来bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx wtxNew)发送coin给别人scriptPubKey参数就不是简单的账户公钥数据是个CScript脚本类型带有公钥数据。2.scriptPubKey构造我们来看一下之前这个scriptPubKey是怎么构造的得到了接收方的地址后void CSendDialog::OnButtonSend(wxCommandEvent event) { CWalletTx wtx; string strAddress (string)m_textCtrlAddress-GetValue(); // Parse amount int64 nValue 0; if (!ParseMoney(m_textCtrlAmount-GetValue(), nValue) || nValue 0) { wxMessageBox(Error in amount ); return; } if (nValue GetBalance()) { wxMessageBox(Amount exceeds your balance ); return; } if (nValue nTransactionFee GetBalance()) { wxMessageBox(string(Total exceeds your balance when the ) FormatMoney(nTransactionFee) transaction fee is included ); return; } // Parse bitcoin address uint160 hash160; bool fBitcoinAddress AddressToHash160(strAddress, hash160); if (fBitcoinAddress) { // Send to bitcoin address CScript scriptPubKey; scriptPubKey OP_DUP OP_HASH160 hash160 OP_EQUALVERIFY OP_CHECKSIG; if (!SendMoney(scriptPubKey, nValue, wtx)) return; wxMessageBox(Payment sent , Sending...); }获取到地址后就是strAddress1开头的那种地址然后调用函数AddressToHash160转成hash160格式就是将strAddress base58解码得到25字节的数据后去掉前缀一个字节和去掉后四个字节的校验和得到20字节的数据就是hash160,其实就是公钥生成账号里的第二步骤里那个hash160数据把它提取出来AddressToHash160函数的功能就是这个。得到hash160后关键的代码CScript scriptPubKey; scriptPubKey OP_DUP OP_HASH160 hash160 OP_EQUALVERIFY OP_CHECKSIG;3.脚本锁定和解锁其中OP_开头的都是操作码指明进行什么操作这是一个锁定脚本。放在vout里如下成员class CTxOut { public: int64 nValue; // 金额单位聪 CScript scriptPubKey; // ← 这里就是你看到的 scriptPubKey // ... 其他成员和函数 };我们知道这是个脚本但是这个脚本只是一部分它还需要对应的脚本就是解锁脚本(即CTxIn里class CTxIn { public: COutPoint prevout; // 指向上一笔交易的输出哪个 UTXO CScript scriptSig; // ← 这里就是解锁脚本Unlocking Script uint32_t nSequence; // 序列号老版本常用 0xFFFFFFFF // ... 其他成员 };这两个脚本内容最终合成一个脚本执行后如果结果为true则解锁成功后续解锁人可以花费这笔vout)。好关于下面的操作码这里先不解释scriptPubKey OP_DUP OP_HASH160 hash160 OP_EQUALVERIFY OP_CHECKSIG;4.脚本代码解释因为我们要先理解一些概念什么叫合成一个脚本什么叫结果为true这些概念。我们先来看这个脚本代码2 3 OP_ADD 5 OP_EQUALOP_ADD这个操作码是两数相加OP_EQUAL是判断两数量相等。但是这里我们要搞清楚的是操作哪两个数相加结果又放在哪里。我们这里要引入栈顶的概念push和pop。第一个2就是将2这个数字压入栈顶即push操作。然后再压入3接着OP_ADD的意思是从栈顶开始将两个数字相加并将结果压入栈顶。那么如果你是2 3 8 OP_ADD那么是3和8相加。并且结果是2 11注意并不是2 3 8 11。此时栈中的状态。因为OP_ADD操作两上数字它会把这个两个数字pop出去相当于删除。再压入结果那么此时11被压入栈顶。所以我们再来看这个脚本2 3 OP_ADD 5 OP_EQUAL2 3相加得到5 再压入5然后调用OP_EQUAL判断两者是否相等。如果相等就把true压入栈顶即1的值。把这操作脚本转换成相关的类就是CScript script; script 2 3 OP_ADD 5 OP_EQUAL;5.运行脚本前言:可以看到我们脚本就是正常的脚本没有什么锁定脚本和解锁脚本这些都是自定义的你可以人为的把这完整的脚本分开比如锁定脚本就是5 OP_EQUAL,然后解锁脚本就是2 3 OP_ADD,然后验证的时候拼接在一起就行了。这就是锁定脚本和解释脚本的来源。当然比特币真正这部分脚本比较复杂不是简单的验证一个是否等于5就行了要验证签名的。所以这里的验证签名并不是外面单独调用一个“验证签名”函数而是通过脚本来完成签名验证的。当然脚本内部实现肯定也是调用了“验证签名” 函数。为什么要这样设计呢或者说为什么要引入脚本呢增加灵活性使其变成了一种可编程的货币关于涉及到哪些功能我们将在后续慢慢介绍。我们要运行脚本就必须解决CScript类相关问题即兼容性将script.h和script.cpp引用到我们的项目中并解决兼容性问题。6.CScript我们先来理解一下源码中的这个类class CScript : public vectorunsigned char它为什么要继承vectorunsigned char呢因为SCcript作为脚本容器用vector数组来存储脚本代码最好不过了。所以它要继承这个类。注意这个并不是栈的容器栈是另一个容器stack相关。好我们在自己的项目中新建script.h和script.cpp然后将代码复制进去。7.修改报错代码然后接下来是修改报错的代码了(可跳过后续可直接下载改好的文件)7.1script.cpp 928行(foreach改成for)foreach(const CScript script2, vTemplates)改成如下for(const CScript script2:vTemplates)7.2986行同样改法:foreach(PAIRTYPE(opcodetype, valtype) item, vSolution)然后上面的PAIRTYPE未定义标识符我们需要在util.h中添加如下代码// This is needed because the foreach macro cant get over the comma in pairt1, t2 #define PAIRTYPE(t1, t2) pairt1, t21045行同样改for语句:foreach(PAIRTYPE(opcodetype, valtype) item, vSolution)script.h中 303行:return HexNumStr(vch.begin(), vch.end());HexNumStr未定义标识符将此代码复制到util.h中:templatetypename T string HexNumStr(const T itbegin, const T itend, bool f0xtrue) { const unsigned char* pbegin (const unsigned char*)itbegin[0]; const unsigned char* pend pbegin (itend - itbegin) * sizeof(itbegin[0]); string str (f0x ? 0x : ); for (const unsigned char* p pend-1; p pbegin; p--) str strprintf(%02X, *p); return str; }310行改forforeach(const vectorunsigned charvch, vStack)555行:printf(CScript(%s)\n, HexStr(begin(), end()).c_str());HexStr未定义util.h中添加如下代码:templatetypename T string HexStr(const T itbegin, const T itend, bool fSpacestrue) { const unsigned char* pbegin (const unsigned char*)itbegin[0]; const unsigned char* pend pbegin (itend - itbegin) * sizeof(itbegin[0]); string str; for (const unsigned char* p pbegin; p ! pend; p) str strprintf((fSpaces p ! pend-1 ? %02x : %02x), *p); return str; }script.cpp 1078行改forforeach(PAIRTYPE(opcodetype, valtype) item, vSolution)然后我们来看这里报错(1099行处)uint256 hash SignatureHash(scriptPrereq txout.scriptPubKey, txTo, nIn, nHashType); if (!Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)) return false; txin.scriptSig scriptPrereq txin.scriptSig;这个的运算符即scriptPrereq这个操作显示没有与这些操作数匹配的运行符。然后我看了CScript是有重载操作符的如下friend CScript operator(const CScript a, const CScript b) { CScript ret a; ret b; return (ret); }但是奇怪的是左右两边都是CScript类型而我的实际应用右边txout.scriptPubKey是string类型。源码中为什么没有string类型的重载或者说没写相关的转换功能中本聪应该不可能犯这种错。我想了一下恍然大悟原本txout.scriptPubKey就是CScript类型只不过我之前为了简便用string代替了。改回即可如下:class CTxOut { public: int64 nValue; CScript scriptPubKey;包括后面的txin.scriptSigCTxIn类里一样改回。接下来还有几个小报错也是跟CTxOut和CTxIn类相关因为之前这些类都是自己写的简写类。缺了一些成员和函数把报错的补上即可解决。CTxIn没有nSequence加上即可:unsigned int nSequence;CTxOut没有SetNull函数加上此函数即可:void SetNull() { nValue -1; scriptPubKey.clear(); }好没有显式的报错了接下来编译一下。不通过报没有匹配的号运算符。这是因为我们之前写的自定义函数CreateBlock和 GenerateTestBlock之类的将vin和vout里的签名用字符串代替导致现在我改回来CScript类型了但这里的函数还是给它们赋值字符串把这些函数删掉或把相应赋值注释掉就行txNew.vin[0].prevout.n -1; //txNew.vin[0].scriptSig pszTimestamp; //注释掉 否则类型不匹配 //vout赋值 txNew.vout[0].nValue 100; // txNew.vout[0].scriptPubKey zhengyong;这次编译也没问题了接下来我们来测试一下代码写一些脚本来调用。看能否正常执行。8.EvalScript我们先来看源码中验证签名脚本的逻辑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); }最后是调用了EvalScript这个函数来执行脚本的在第一个参数里也可以看到将txin.scriptSig和txout.scriptPubKey是拼在了一起的。这就是锁定脚本和解锁脚本实现的原理。9.测试代码int main() { printf(Bitcoin v0.1.0 Simple Script Test\n); printf(Script: 2 3 OP_ADD 5 OP_EQUAL\n\n); // 1. 构造脚本 CScript script; script 2 3 OP_ADD 5 OP_EQUAL; // 2. 执行脚本 vectorvectorunsigned char stack; bool success EvalScript(script, CTransaction(), 0, 0, stack); // 3. 输出结果 if (success) { printf(EvalScript executed successfully!\n); if (!stack.empty()) { const std::vectorunsigned char top stack.back(); bool finalResult (top.size() 0 top[0] ! 0); printf(Final stack size : %zu\n, stack.size()); printf(Top of stack (bool): %s\n, finalResult ? TRUE : FALSE); if (finalResult) printf(Script result: PASSED (as expected)\n); else printf(Script result: FAILED\n); } else { printf(Stack is empty!\n); } } else { printf(EvalScript failed!\n); } // 可选打印栈中所有元素调试用 printf(\nStack content (bottom to top):\n); for (size_t i 0; i stack.size(); i) { // 简单打印前几个字节 printf([%zu] size%zu , i, stack[i].size()); for (size_t j 0; j stack[i].size() j 8; j) printf(%02x , stack[i][j]); printf(\n); } return 0; }运行结果:执行成功返回真。10.stack栈结构我们可以看到stack就是用来接收栈的内容的。其实栈的容器就是一个vector的unsigned char二维数组。我们在EvalScript中也能看到如下定义vectorvaltype stack;其中的valtype类型其实就是typedef vectorunsigned char valtype;好知道了stack栈是个二维数组那它是怎么压入变量的呢一个二维数组的元素算一个变量不是这样的。而是stack[0]算一个按行来压入元素为什么这样设计很明显如果你压入单个字节的变量到栈中那是没问题的如果你要压入一个很大的数字比如10000000或者压入公钥到栈中呢一个字节满足不了。所以它是按行来算的比如:script 2 3 OP_ADD 5 OP_EQUAL;压入2到stack[0]中然后压入3到stack[1]中此时我们调用vector.back()返回最后一个元素就是stack[1]为栈顶然后调用OP_ADD把两个数字相加即pop弹出两个元素(删除)然后再将结果压入栈顶。那么此时stack[0]就被压入5了然后是压入5到stack[1]中最后调用OP_EQUAL操作码判断是否相等把结果压入到栈顶中。注意上面压入单个字节的变量即stack[0][0],我要表达的意思是再压入不是stack[0][1],而是stack[1][0]这样好这些是一个字节如果压入一个大数字或者公钥那么二维数组此时的列数是根据数据大小来的。即如果这个元素是两个字节那么stack[0][0],stack[0][1]用来存储如果是32字节则stack[0][0]~stack[0][31]是这个范围。然后再压入一个元素会另起一行。11.EvalScript逻辑但是你会发现如果我们把上面的脚本5改成6即script 2 3 OP_ADD 6 OP_EQUAL;执行后你会发现栈是空的按理说就算结果为false它也应该把0这个数值存储到栈中栈顶不会为空的。这是因为EvalScript就是为了锁定和解锁而设计的所以如果结果为假它就会清空栈里的结果(为真则会保留栈中的结果)。为假后你并不能访问stack里的结果元素。所以我们只要判断一下stack[0].size()为空即为0也可知道结果为假如果我们需要一个纯粹的脚本执行器我们需要修改EvlaScript函数相关代码。这里留待以后。这里我们知道就行就像下面script 2 3 OP_ADD ;这个脚本没有判断相等但是栈的结果为5也为真EvalScript它的返回值也是为真的。最后元素输出为5:另注意这里代码const std::vectorunsigned char top stack.back(); bool finalResult (top.size() 0 top[0] ! 0);这里的top[0]并不是stack[0],而是stack[0][0],因为back返回最后一个元素即stack[0]赋给top那么top[0]当然就是stack[0][0]了。