比特幣原始碼學習筆記
比特幣原始碼學習筆記
FROM http://www.8btc.com/author/17786?order=hot
前言
從事區塊鏈的開發,不瞭解其底層核心技術是不夠的。許多人在看了比特幣白皮書之後仍然不清楚比特幣是怎樣實現的,因為比特幣的原始碼設計精巧,有許多設計白皮書未曾提及,加上本身比特幣的文件稀少,加大了新手理解的困難程度。儘管現在已經有許多介紹區塊鏈的書和文章,卻很少是從原始碼著手分析的。我通過半年時間對於區塊鏈的學習,開始撰寫一份比特幣原始碼的教程。本教程深入淺出,通過分析最經典的區塊鏈——比特幣的C++客戶端原始碼,讓開發者用最短的時間上手區塊鏈技術。瞭解比特幣原始碼可幫助開發者更好了解區塊鏈的工作原理並在應用當中根據實際情況做出修改和調整。
本文所引用的原始碼均來自原始版比特幣客戶端,即由中本聰釋出的第一版原始碼。該客戶端包括大約16000行程式碼。儘管經過數年的發展,比特幣客戶端經過了幾次較大更新,其資料結構和原理從誕生之日起一直延續至今。本文會盡可能保證文字的嚴謹準確,表達當中難免會產生疏漏,歡迎指正。
第一章
本章節講述比特幣客戶端是怎樣生成比特幣地址,並建立新的交易。
我們來看一下GenerateNewKey()方法,該方法位於main.cpp。
bool AddKey(const CKey& key) { CRITICAL_BLOCK(cs_mapKeys) { mapKeys[key.GetPubKey()] = key.GetPrivKey(); mapPubKeys[Hash160(key.GetPubKey())] = key.GetPubKey(); } return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); } vector<unsigned char> GenerateNewKey() { CKey key; key.MakeNewKey(); if (!AddKey(key)) throw runtime_error("GenerateNewKey() : AddKey failed\n"); return key.GetPubKey(); }
該方法通過以下步驟生成一個新的公鑰對:
- 首先建立一個新的CKey型別物件(第13行)。
- 呼叫addKey()方法將新建的key新增至1)全域性對映
mapKeys
(第5行)2)全域性map mapPubKeys
(第6行)和錢包資料庫wallet.dat(第8行)。
mapKeys建立公鑰與私鑰的一一對應關係。
mapPubKeys建立公鑰的hash和公鑰本身的對應關係。
- 返回公鑰(第16行)。
該公鑰為未壓縮的格式,屬於OpenSSL標準格式之一。在得到公鑰之後,比特幣客戶端會將該公鑰傳遞至PubKeyToAddress()
Hash160ToAddress()
方法生成地址。最後返回的Base58編碼字串值便是一個新生成的比特幣地址。Base58由1-9和除i,l,0,o之外的英文字元組成。
CTransaction類
CTransaction的定義位於main.h。在比特幣當中,所謂幣的概念其實是一系列交易Tx的組合。這種方式雖然實現起來更為複雜,卻提高了比特幣的安全性。使用者可以為每一筆交易建立一個新的地址,地址在使用一次之後可以立即作廢。因此,CTransaction是比特幣客戶端最重要的類之一。
class CTransaction { public: int nVersion; vector<CTxIn> vin; vector<CTxOut> vout; int nLockTime; //...... }
CTransaction包含兩個容器型別:輸入交易vin和輸出交易vout。每個vin由若干CTxIn物件組成,每個vout則由CTxOut組成。
每筆交易Tx的輸入交易(CTxIn類)包含一個COutPoint物件prevout,該物件引用另外一筆交易Tx的輸出交易作為來源交易。來源交易使當前交易Tx從另一筆交易當中得到可花費的比特幣。一筆交易Tx可以擁有任意筆輸入交易。
任何交易均由一個256位uint256雜湊作為其唯一識別。若要引用某一筆來源交易TxSource當中某個特定的輸出交易,我們需要兩種資訊:TxSource的雜湊,和該輸出交易在輸出交易當中的位置n。這兩種資訊構成COutPoint類。一個COutPoint物件指向來源交易的某一筆輸出交易TxSource.vout[n]
。如果該筆輸出交易被另外一筆交易Tx的位置i的輸入交易所引用,例如Tx.vin[i].prevout
,我們將其稱為Tx的第i筆輸入交易花費了TxSource中的第n筆輸出交易。
uint256和uint160類
這兩種型別的定義位於uint.h。一個uint256類包含有一個256位的雜湊。它由一個長度為256/32=8的unsigned int陣列構成。一個相似的資料結構是uint160,該結構的定義可在同一個檔案當中找到。既然SHA-256的長度為256bit,讀者不難推斷出uint160的作用是存放RIPEMD-160雜湊。uint256和uint160均由base_uint類繼承而來。
class base_uint { protected: enum { WIDTH = BITS / 32 }; unsigned int pn[WIDTH]; public: bool operator!() const { for (int i = 0; i < WIDTH; i++) if (pn[i] != 0) return false; return true; } //...... unsigned int GetSerializeSize(int nType = 0, int nVersion = VERSION) const { return sizeof(pn); } template <typename Stream> void Serialize(Stream& s, int nType = 0, int nVersion = VERSION) const { s.write((char*)pn, sizeof(pn)); } template <typename Stream> void Unserialize(Stream& s, int nType = 0, int nVersion = VERSION) { s.read((char*)pn, sizeof(pn)); } }
該類過載了若干運算子。此外該類擁有3個序列化成員函式,GetSerializeSize()
、Serialize()
和Unserialize()
。我們會在後面講到這三種方法是如何工作的。
SendMoney()
該方法位於main.cpp。以下是該方法的原始碼:
bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew) { CRITICAL_BLOCK(cs_main) { int64 nFeeRequired; if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired)) { string strError; if (nValue + nFeeRequired > GetBalance()) strError = strprintf("Error: This is an oversized transaction that requires a transaction fee of %s ", FormatMoney(nFeeRequired).c_str()); else strError = "Error: Transaction creation failed "; wxMessageBox(strError, "Sending..."); return error("SendMoney() : %s\n", strError.c_str()); } if (!CommitTransactionSpent(wtxNew)) { wxMessageBox("Error finalizing transaction", "Sending..."); return error("SendMoney() : Error finalizing transaction"); } printf("SendMoney: %s\n", wtxNew.GetHash().ToString().substr(0,6).c_str()); // Broadcast if (!wtxNew.AcceptTransaction()) { // This must not fail. The transaction has already been signed and recorded. throw runtime_error("SendMoney() : wtxNew.AcceptTransaction() failed\n"); wxMessageBox("Error: Transaction not valid", "Sending..."); return error("SendMoney() : Error: Transaction not valid"); } wtxNew.RelayWalletTransaction(); } MainFrameRepaint(); return true; }
當用戶傳送比特幣到某一個地址時,比特幣客戶端會呼叫SendMoney()方法。該方法包含三個引數:
- scriptPubKey包含指令碼程式碼OP_DUP OP_HASH160 <收款人地址160位雜湊> OP_EQUALVERIFY OP_CHECKSIG。
- nValue表示將要轉賬的金額。該金額並未包含交易費nTrasactionFee。
- wtxNew是一個CWalletTx類的本地變數。該變數目前的值為空,之後會包含若干CMerkleTX類物件。該類由CTransaction衍生而來,並且添加了若干方法。我們暫時先不管具體細節,僅將其看作CTransaction類。
該方法的流程顯而易見:
- 首先建立一筆新的交易(CreateTransaction(scriptPubKey, nValue, wtxNet, nFeeRequired),第6行)。
- 嘗試將這筆交易提交至資料庫(CommitTransactionSpent(wtxNet),第16行)。
- 如果該筆交易提交成功(wtxNew.AcceptTransaction(),第23行),將其廣播至其他peer節點(wtxNew.RelayWalletTransaction(),第30行)。
這四個方法都與wtxNew相關。我們在本章介紹了第一個,其餘三個將會在後續文章中介紹。
CreateTransaction()
該方法位於main.cpp。以下是該方法的原始碼:
bool CreateTransaction(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, int64& nFeeRequiredRet) { nFeeRequiredRet = 0; CRITICAL_BLOCK(cs_main) { // txdb must be opened before the mapWallet lock CTxDB txdb("r"); CRITICAL_BLOCK(cs_mapWallet) { int64 nFee = nTransactionFee; loop { wtxNew.vin.clear(); wtxNew.vout.clear(); if (nValue < 0) return false; int64 nValueOut = nValue; nValue += nFee; // Choose coins to use set<CWalletTx*> setCoins; if (!SelectCoins(nValue, setCoins)) return false; int64 nValueIn = 0; foreach(CWalletTx* pcoin, setCoins) nValueIn += pcoin->GetCredit(); // Fill vout[0] to the payee wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey)); // Fill vout[1] back to self with any change if (nValueIn > nValue) { // Use the same key as one of the coins vector<unsigned char> vchPubKey; CTransaction& txFirst = *(*setCoins.begin()); foreach(const CTxOut& txout, txFirst.vout) if (txout.IsMine()) if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) break; if (vchPubKey.empty()) return false; // Fill vout[1] to ourself CScript scriptPubKey; scriptPubKey << vchPubKey << OP_CHECKSIG; wtxNew.vout.push_back(CTxOut(nValueIn - nValue, scriptPubKey)); } // Fill vin foreach(CWalletTx* pcoin, setCoins) for (int nOut = 0; nOut < pcoin->vout.size(); nOut++) if (pcoin->vout[nOut].IsMine()) wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut)); // Sign int nIn = 0; foreach(CWalletTx* pcoin, setCoins) for (int nOut = 0; nOut < pcoin->vout.size(); nOut++) if (pcoin->vout[nOut].IsMine()) SignSignature(*pcoin, wtxNew, nIn++); // Check that enough fee is included if (nFee < wtxNew.GetMinFee(true)) { nFee = nFeeRequiredRet = wtxNew.GetMinFee(true); continue; } // Fill vtxPrev by copying from previous transactions vtxPrev wtxNew.AddSupportingTransactions(txdb); wtxNew.fTimeReceivedIsTxTime = true; break; } } } return true; }
呼叫該方法時,它所需要的四個引數如下:
- scriptPubKey包含指令碼程式碼OP_DUP OP_HASH160 <收款人地址160位雜湊> OP_EQUALVERIFY OP_CHECKSIG。
- nValue是將要轉賬的數額,交易費nTransactionFee並未包括在內。
- wtxNew是一個新的Tx例項。
- nFeeRequiredRet是一筆用來支付交易費的輸出交易,在該方法執行完成之後獲得。
該方法的流程如下:
- 定義一個本地變數nValueOut = nValue來儲存將轉賬的金額(第17行)。將nValue與交易費nFee相加得到新的包含轉賬費的nValue。
- 執行位於第21行的SelectCoins(nValue, setCoins)得到一系列幣,並放入setCoins。setCoins包含支付給你本人地址的交易,即你所擁有的幣。這些交易將成為wtxNew的來源交易。
- 執行位於第27行的wtxNew.vout.push_back(CTxOut (nValueOut,sciptPubKey))並新增一筆輸出交易至wtxNew。該筆輸出將支付給<收款人地址160位雜湊>(包含在scriptPubKey裡面)數量為的幣。
- 如果需要找零(nValueIn > nValue),新增另一筆輸出交易至wtxNew並將零錢發回本人。該過程包含以下步驟:
- 從setCoin當中獲取第一筆交易txFirst,依次檢查txFirst.vout中的交易是否屬於本人。如果是則從該筆輸出交易當中提取出公鑰,並放入本地變數vchPubKey
- 將vchPubKey放入指令碼vchPubKey OP_CHECKSIG,並使用這段指令碼程式碼為wtxNew新增一個支付給本人的輸出交易(第45行)。
- 因為setCoins包含支付給本人的交易,所以每筆交易一定包括至少一筆支付給本人的交易。從第一筆交易txFirst中即可找到。
- 至此,wtxNew的輸出交易容器vout已準備就緒。現在,該設定輸入交易容器vin。記住每一個輸入交易列表vin均引用一筆來源交易,而且wtxNew的每筆來源交易均可在setCoins中被找到。對於每一筆setCoins中的交易pcoin,逐個遍歷其輸出交易pcoin->vout[nOut]。如果第nOut筆輸出支付給本人(意味著wtxNew從該筆輸出交易中獲得幣),則向wtxNew新增一筆新的輸入交易(wtxNew.vin(wtxNew.vin.push_back(CTxIn(pcoin->GetHash(), nOut)),第51行)。該輸入交易指向pcoin中的第nOut筆輸出交易,由此將wtxNew.vin與pcoin的第nOut筆輸出相連線。
- 對於setCoins當中的每筆交易pcoin,逐個遍歷其所有輸出交易pcoin->vout[nOut]。如果該筆交易屬於本人,呼叫SignSignature(*pcoin,wtxNew, nIn++)為第nIn筆輸入交易添加簽名。注意nIn為wtxNew的輸入交易位置。
- 如果交易費nFee小於wtxNet.GetMinFee(true),將nFee設為後者,清空wtxNew中的所有資料並重新開始整個過程。在位於第11行的第一次迭代當中,nFee是全域性變數nTransactionFee = 0的本地複製。
- 如果你不明白為什麼要如此費力地重新添滿wtxNew,原始碼中的GetMinFee()提供了答案:交易的最低費用與交易的資料大小有關。wtxNew的大小隻有在完整構建之後才可得知。如果wtxNew.GetMinFee(true)計算得到的最小交易費用大於之前創造wtxNew時假設的交易費nFee,則除了重新構建wtxNew之外別無他法。
- 這裡遇到了一個先有雞還是先有蛋的局面:若想建立一筆新的交易,則必須知道交易費用是多少。而交易費只有在整個交易被建立以後才可得知。為了打破這個迴圈,本地變數nFee被用來放置預計的交易費用,並且新的交易構建在此基礎上。在構建完成之後,得到真實的交易費並與預估的交易費作比較。如果預估的交易費小於真實的交易費,則替換成真實交易費並重新構造整個交易。
這裡是GetMinFee()的原始碼:
int64 GetMinFee(bool fDiscount=false) const { unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK); if (fDiscount && nBytes < 10000) return 0; return (1 + (int64)nBytes / 1000) * CENT; }
- 如果計算得到的交易費比之前預計的交易費更高,則跳出第11行開始的迴圈並返回整個函式(第67行)。在此之前,需要進行以下兩個步驟:
- 執行wtxNew.AddSupportingTransactions(txdb)。這一部分以後會進行更詳細介紹。
- 設定wtxNet.fTimeReceivedIsTxTime=true(第66行)。
現在來看一下如何通過SignSignature()簽署新生成的交易wtxNew。
SignSignature()
該方法位於script.cpp。以下是該方法的原始碼:
bool SignSignature(const CTransaction& txFrom, CTransaction& txTo, unsigned int nIn, int nHashType, CScript scriptPrereq) { assert(nIn < txTo.vin.size()); CTxIn& txin = txTo.vin[nIn]; assert(txin.prevout.n < txFrom.vout.size()); const CTxOut& txout = txFrom.vout[txin.prevout.n]; // Leave out the signature from the hash, since a signature can't sign itself. // The checksig op will also drop the signatures from its hash. uint256 hash = SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType); if (!Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)) return false; txin.scriptSig = scriptPrereq + txin.scriptSig; // Test solution if (scriptPrereq.empty()) if (!EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn)) return false; return true; }
首先需要注意的是,該函式有5個引數,而CreateTransaction()
只有3個。這是因為在script.h檔案裡,後兩個引數已預設給出。
以下是傳遞給CreateTransaction()
中的3個引數:
- txFrom是一個*pcoin物件。它是CreateTransaction()裡setCoins中的所有幣中的某一個。它同時也是一筆來源交易。它的若干輸出交易當中包含了新交易將要花費的幣。
- txTo是CreateTransaction()裡的wtxNew物件。它是將要花費來源交易txFrom的新交易。新交易需要被簽署方可生效。
- nIn是指向txTo中輸入交易列表的索引位置。該輸入交易列表包含一個對txFrom的輸出交易列表的引用。更準確地講,txin=txTo.vin[nIn](第4行)是txTo中的輸入交易;
txout=txFrom.vout[txin.prev.out.n]
(第6行)是txin所指向的txFrom中的輸出交易。
以下是SignSignature()所做的工作:
- 呼叫SignatureHash()方法生成txTo的雜湊值。
- 呼叫Solver()函式簽署剛才生成的雜湊。
- 呼叫EvalScript()來執行一小段指令碼並檢查簽名是否合法。
我們一起看一下這三個函式。
SignatureHash()
該方法位於script.cpp
。以下是SignatureHash()的原始碼。
uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType) { if (nIn >= txTo.vin.size()) { printf("ERROR: SignatureHash() : nIn=%d out of range\n", nIn); return 1; } CTransaction txTmp(txTo); // In case concatenating two scripts ends up with two codeseparators, // or an extra one at the end, this prevents all those possible incompatibilities. scriptCode.FindAndDelete(CScript(OP_CODESEPARATOR)); // Blank out other inputs' signatures for (int i = 0; i < txTmp.vin.size(); i++) txTmp.vin[i].scriptSig = CScript(); txTmp.vin[nIn].scriptSig = scriptCode; // Blank out some of the outputs if ((nHashType & 0x1f) == SIGHASH_NONE) { // Wildcard payee txTmp.vout.clear(); // Let the others update at will for (int i = 0; i < txTmp.vin.size(); i++) if (i != nIn) txTmp.vin[i].nSequence = 0; } else if ((nHashType & 0x1f) == SIGHASH_SINGLE) { // Only lockin the txout payee at same index as txin unsigned int nOut = nIn; if (nOut >= txTmp.vout.size()) { printf("ERROR: SignatureHash() : nOut=%d out of range\n", nOut); return 1; } txTmp.vout.resize(nOut+1); for (int i = 0; i < nOut; i++) txTmp.vout[i].SetNull(); // Let the others update at will for (int i = 0; i < txTmp.vin.size(); i++) if (i != nIn) txTmp.vin[i].nSequence = 0; } // Blank out other inputs completely, not recommended for open transactions if (nHashType & SIGHASH_ANYONECANPAY) { txTmp.vin[0] = txTmp.vin[nIn]; txTmp.vin.resize(1); } // Serialize and hash CDataStream ss(SER_GETHASH); ss.reserve(10000); ss << txTmp << nHashType; return Hash(ss.begin(), ss.end()); }
以下是該函式所需要的引數:
- txTo是將要被簽署的交易。它同時也是CreateTransaction()中的wtxNew物件。它的輸入交易列表中的第nIn項,
txTo.vin[nIn]
,是該函式將要起作用的目標。 - scriptCode是scriptPrereq + txout.scriptPubKey,其中txout是SignSignature()中定義的來源交易txFrom()的輸出交易。由於此時scriptPrereq為空,scriptCode事實上是來源交易txFrom中的輸出交易列表當中被txTo作為輸入交易引用的那筆的指令碼程式碼。txout.scriptPubKey有可能包含兩類指令碼:
- 指令碼A:OP_DUP OP_HASH160 <你地址的160位雜湊> OP_EQUALVERIFY OP_CECKSIG。該指令碼將來源交易txFrom中的幣傳送給你,其中<你地址的160位雜湊>是你的比特幣地址。
- 指令碼B:<你的公鑰> OP_CHECKSIG。該指令碼將剩餘的幣退還至來源交易txFrom的發起人。由於你建立的新交易txTo/wtxNew將會花費來自txFrom的幣,你必須同時也是txFrom的建立者。換句話講,當你在建立txFrom的時候,你其實是在花費之前別人傳送給你的幣。因此,<你的公鑰>即是txFrom建立者的公鑰,也是你自己的公鑰。
我們在此停留片刻,來思考一下指令碼A和指令碼B。你有可能會問,這些指令碼是從哪來的。中本聰在創造比特幣的時候為比特幣添加了一套指令碼語言系統,所以比特幣中的交易都是由指令碼程式碼完成的。該腳本系統其實也是後來智慧合約的雛形。指令碼A來自第29行,位於方法CSendDialog::OnButtonSend(),指令碼B則來自第44行,位於方法CreateTransaction()。
- 當用戶發起一筆交易時,比特幣客戶端會呼叫CSendDialog::OnButtonSend()方法並將指令碼A新增至txFrom中的一筆輸出交易中。由於該輸出交易的收款方為你本人,從而指令碼中的<收款人地址160位雜湊>,就是<你的地址160位雜湊>。
- 如果txFrom是你本人建立的,則指令碼B會被新增至CreateTransaction()中txFrom的某一筆輸出交易。在這裡,第44行位於CreateTransaction()中的公鑰vchPubKey是你本人的公鑰。
在瞭解了輸入交易之後,我們來一起了解SignatureHash()是怎樣工作的。
SignatureHash()首先將txTO拷貝至txTmp,接著清空txTmp.vin中每一筆輸入交易的scriptSig,除了txTmp.vin[nIn]之外,該輸入交易的scriptSig被設為scriptCode(第14、15行)。
接著,該函式檢驗nHashType的值。該函式的呼叫者將一個列舉值傳遞至該函式nHashType = SIGHASH_ALL。
enum { SIGHASH_ALL = 1, SIGHASH_NONE = 2, SIGHASH_SINGLE = 3, SIGHASH_ANYONECANPAY = 0x80, };
由於nHashType = SIGHASH_ALL,所有的if-else條件均不成立,該函式將直接執行最後4行程式碼。
在最後4行程式碼中,txTmp和nHashType變成序列化後的型別CDataStream物件。該型別包括一個裝有資料的字元容器型別。所返回的雜湊值是Hash()方法在計算序列化後的資料所得到的。
一筆交易可以包含多筆輸入交易。SignatureHash()取其中一筆作為目標。它通過以下步驟生成雜湊:
- 清空除了目標交易之外的所有輸入交易。
- 複製來源交易中被目標交易作為輸入交易引用的那筆輸出交易的指令碼至目標交易的輸入交易列表中。
- 為修改後的交易生成雜湊值。
Hash()
該方法位於util.h。以下是生成雜湊值的方法Hash()的原始碼:
template<typename T1> inline uint256 Hash(const T1 pbegin, const T1 pend) { uint256 hash1; SHA256((unsigned char*)&pbegin[0], (pend - pbegin) * sizeof(pbegin[0]), (unsigned char*)&hash1); uint256 hash2; SHA256((unsigned char*)&hash1, sizeof(hash1), (unsigned char*)&hash2); return hash2; }
該函式對目標資料執行兩次SHA256()方法並返回結果。SHA256()的宣告可在openssl/sha.h中找到。
Solver()
該方法位於script.cpp。Solver()在SignSignature()中緊接著SignatureHash()被執行。它是真正用來為SignatureHash()返回的雜湊值生成簽名的函式。
bool Solver(const CScript& scriptPubKey, uint256 hash, int nHashType, CScript& scriptSigRet) { scriptSigRet.clear(); vector<pair<opcodetype, valtype> > vSolution; if (!Solver(scriptPubKey, vSolution)) return false; // Compile solution CRITICAL_BLOCK(cs_mapKeys) { foreach(PAIRTYPE(opcodetype, valtype)& item, vSolution) { if (item.first == OP_PUBKEY) { // Sign const valtype& vchPubKey = item.second; if (!mapKeys.count(vchPubKey)) return false; if (hash != 0) { vector<unsigned char> vchSig; if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) return false; vchSig.push_back((unsigned char)nHashType); scriptSigRet << vchSig; } } else if (item.first == OP_PUBKEYHASH) { // Sign and give pubkey map<uint160, valtype>::iterator mi = mapPubKeys.find(uint160(item.second)); if (mi == mapPubKeys.end()) return false; const vector<unsigned char>& vchPubKey = (*mi).second; if (!mapKeys.count(vchPubKey)) return false; if (hash != 0) { vector<unsigned char> vchSig; if (!CKey::Sign(mapKeys[vchPubKey], hash, vchSig)) return false; vchSig.push_back((unsigned char)nHashType); scriptSigRet << vchSig << vchPubKey; } } } } return true; }
以下是該方法所需要的4個引數:
- 位於第10行的呼叫函式SignSignature()將txOut.scriptPubKey,來源交易txFrom的輸出指令碼,作為輸入值傳入第一個引數scriptPubKey。記住它可能包含指令碼A或者指令碼B。
- 第二個引數hash是由SignatureHash()生成的雜湊值。
- 第三個引數nHashType的值為SIGHASH_ALL。
- 第四個引數是該函式的返回值,即呼叫函式SignSIgnature()中位於第12行的txin.scriptSig。記住txin是新生成的交易wtxNew(在呼叫函式SignSignature()中作為txTo引用)位於第nIn的輸入交易。因此,wtxNew第nIn筆輸入交易的scriptSig將存放該函式返回的簽名。
該函式首先會呼叫另一個有2個引數的Solver()。我們來研究一下。
帶有2個引數的Solver()
該方法位於script.cpp。以下是帶有2個引數的Solver()的原始碼:
bool Solver(const CScript& scriptPubKey, vector<pair<opcodetype, valtype> >& vSolutionRet) { // Templates static vector<CScript> vTemplates; if (vTemplates.empty()) { // Standard tx, sender provides pubkey, receiver adds signature vTemplates.push_back(CScript() << OP_PUBKEY << OP_CHECKSIG); // Short account number tx, sender provides hash of pubkey, receiver provides signature and pubkey vTemplates.push_back(CScript() << OP_DUP << OP_HASH160 << OP_PUBKEYHASH << OP_EQUALVERIFY << OP_CHECKSIG); } // Scan templates const CScript& script1 = scriptPubKey; foreach(const CScript& script2, vTemplates) { vSolutionRet.clear(); opcodetype opcode1, opcode2; vector<unsigned char> vch1, vch2; // Compare CScript::const_iterator pc1 = script1.begin(); CScript::const_iterator pc2 = script2.begin(); loop { bool f1 = script1.GetOp(pc1, opcode1, vch1); bool f2 = script2.GetOp(pc2, opcode2, vch2); if (!f1 && !f2) { // Success reverse(vSolutionRet.begin(), vSolutionRet.end()); return true; } else if (f1 != f2) { break; } else if (opcode2 == OP_PUBKEY) { if (vch1.size() <= sizeof(uint256)) break; vSolutionRet.push_back(make_pair(opcode2, vch1)); } else if (opcode2 == OP_PUBKEYHASH) { if (vch1.size() != sizeof(uint160)) break; vSolutionRet.push_back(make_pair(opcode2, vch1)); } else if (opcode1 != opcode2) { break; } } } vSolutionRet.clear(); return false; }
第一個引數scriptPubKey可能包含指令碼A也可能是指令碼B。再一次說明,它是SignSignature()中來源交易txFrom的輸出指令碼。
第二個引數用來存放輸出交易。它是一個容器對,每個對由一個指令碼運算子(opcodetype型別)和指令碼操作元(valtype型別)構成。
該函式第8-10行首先定義兩個模板:
- 模板A:OP_DUP OP_HASH160 OP_PUBKEYHASH OP_EQUALVERIFY OP_CHECKSIG。
- 模板B:OP_PUBKEY OP_CHECKSIG。
很明顯,模板A、模板B與指令碼A、指令碼B相對應。為了便於對比,以下是指令碼A和B的內容:
- 指令碼A:OP_DUP OP_HASH160 <你的地址160位雜湊> OP_EQUALVERIFY OP_CHECKSIG。
- 指令碼B:<你的公鑰> OP_CHECKSIG。
該函式的作用是將scriptPubKey與兩個模板相比較:
- 如果輸入指令碼為指令碼A,則將模板A中的OP_PUBKEYHASH與指令碼A中的<你的地址160位雜湊>配對,並將該對放入vSolutionRet。
- 如果輸入指令碼為指令碼B,則從模板B中提取運算子OP_PUBKEY,和從指令碼B中提取運算元<你的公鑰>,將二者配對並放入vSolutionRet。
- 如果輸入指令碼與兩個模板均不匹配,則返回false。
回到Solver()
我們回到有4個引數的Solver()並繼續對該函式的分析。現在我們清楚了該函式的工作原理。它會在兩個分支中選擇一個執行,取決於從vSolutionRet得到的對來自指令碼A還是指令碼B。如果來自指令碼A,item.first == OP_PUBKEYHASH;如果來自指令碼B,item.first == OP_PUBKEY。
- item.first == OP_PUBKEY(指令碼B)。在該情形下,item.second包含<你的公鑰>。全域性變數mapKeys將你的全部公鑰對映至與之對應的私鑰。如果mapKeys當中沒有該公鑰,則報錯(第16行)。否則,用從mapKeys中提取出的私鑰簽署新生成的交易wtxNew的雜湊值,其中雜湊值作為第2個被傳入的引數(CKey::Sign(mapKeys[vchPubKey], hash, vchSig),第23行),再將結果放入vchSig,接著將其序列化成scriptSigRet(scriptSigRet << vchSig,第24行)並返回。
- item.first == OP_PUBKEYHASH(指令碼A)。在該情形下,item.second包含<你的地址160位雜湊>。該比特幣地址將被用於從位於第23行的全域性對映mapPubKeys中找到其所對應的公鑰。全域性對映mapPubKeys將你的地址與生成它們的公鑰建立一一對應關係(檢視函式AddKey())。接著,通過該公鑰從mapKeys中找到所對應的私鑰,並用該私鑰簽署第二個引數hash。簽名和公鑰將一同被序列化至scriptSigRet並返回(scriptSig << vchSig << vchPubkey,第24行)。
EvalScript()
該方法位於script.cpp。現在我們回到SignSignature()。在該函式的第12行之後,txin.scriptsig,即wtxNew的第nIn筆輸入交易中的scriptSig部分,將插入一個簽名。該簽名可能是以下其中之一:
- vchSig vchPubKey(指令碼A的簽名A)
- vchSig(指令碼B的簽名B)
在下文當中,vchSig將被引用為<你的簽名_vchSig>,vchPubKey則為<你的公鑰_vchPubKey>,以強調它們分別是你本人的簽名和公鑰。
我們現在開始調查EvalScript(),該函式是SignSignature()呼叫的最後一個函式,位於第15行。EvalScript()帶有3個引數,分別為:
- 第一個引數為txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey。它有可能是:
- 驗證情形A:<你的簽名_vchSig> <你的公鑰_vchPubKey> OP_CODESEPARATOR OP_DUP OP_HASH160 <你的地址160位雜湊> OP_EQUALVERIFY OP_CHECKSIG,即簽名A + OP_CODESEPARATOR + 指令碼A。
- 驗證情形B:<你的簽名_vchSig> OP_CODESEPARATOR <你的公鑰_vchPubKey> OP_CHECKSIG,即簽名B + OP_CODESEPARATOR + 指令碼B。
- 第二個引數為新建立的交易txTo,即CreateTransaction()中的wtxNew。
- 第三個引數為nIn,即將被驗證的交易在txTo輸入交易列表中的位置。
驗證過程我們會在後面詳細講述。簡單地說,EvalScript()驗證新建立交易wtxNew的第nIn筆輸入交易是否包含有效的簽名。至此,一筆新的比特幣交易便建立完成。
第二章
本章繼上一章交易建立之後介紹比特幣客戶端序列化資料的過程。
比特幣客戶端所有的序列化函式均在seriliaze.h中實現。其中,CDataStream類是資料序列化的核心結構。
CDataStream
CDataStream擁有一個字元類容器用來存放序列化之後的資料。它結合一個容器型別和一個流(stream)介面以處理資料。它使用6個成員函式實現這一功能:
class CDataStream { protected: typedef vector<char, secure_allocator<char> > vector_type; vector_type vch; unsigned int nReadPos; short state; short exceptmask; public: int nType; int nVersion; //...... }
- vch存有序列化後的資料。它是一個擁有自定義記憶體分配器的字元容器型別。該記憶體分配器將由該容器的實現在需要分配/釋放記憶體時呼叫。該記憶體分配器會在向作業系統釋放記憶體前清空記憶體中的資料以防止本機的其他程序訪問此資料,從而保證資料儲存的安全性。該記憶體分配器的實現在此不進行討論,讀者可於serialize.h自行查詢。
- nReadPos是vch讀取資料的起始位置。
- state是錯誤標識。該變數用於指示在序列化/反序列化當中可能出現的錯誤。
- exceptmask是錯誤掩碼。它初始化為ios::badbit | ios::failbit。與state類似,它被用於指示錯誤種類。
- nType的取值為SER_NETWORK,SER_DISK,SER_GETHASH,SER_SKIPSIG,SER_BLOCKHEADERONLY之一,其作用為通知CDataStream進行具體某種序列化操作。這5個符號被定義在一個列舉型別enum裡。每個符號均為一個int型別(4位元組),並且其值為2的次方。
enum { // primary actions SER_NETWORK = (1 << 0), SER_DISK = (1 << 1), SER_GETHASH = (1 << 2), // modifiers SER_SKIPSIG = (1 << 16), SER_BLOCKHEADERONLY = (1 << 17), };
- nVersion是版本號。
CDataStream::read()與CDataStream::write()
成員函式CDataStream::read()和CDataStream::write()是用於執行序列化/反序列化CDataStream物件的低階函式。
CDataStream& read(char* pch, int nSize) { // Read from the beginning of the buffer assert(nSize >= 0); unsigned int nReadPosNext = nReadPos + nSize; if (nReadPosNext >= vch.size()) { if (nReadPosNext > vch.size()) { setstate(ios::failbit, "CDataStream::read() : end of data"); memset(pch, 0, nSize); nSize = vch.size() - nReadPos; } memcpy(pch, &vch[nReadPos], nSize); nReadPos = 0; vch.clear(); return (*this); } memcpy(pch, &vch[nReadPos], nSize); nReadPos = nReadPosNext; return (*this); } CDataStream& write(const char* pch, int nSize) { // Write to the end of the buffer assert(nSize >= 0); vch.insert(vch.end(), pch, pch + nSize); return (*this); }
CDataStream::read()從CDataStream複製nSize個字元到一個由char* pch所指向的記憶體空間。以下是它的實現過程:
- 計算將要從vch讀取的資料的結束位置,unsigned int nReadPosNext = nReadPos + nSize。
- 如果結束位置比vch的大小更大,則當前沒有足夠的資料供讀取。在這種情況下,通過呼叫函式setState()將state設為ios::failbit,並將所有的零複製到pch。
- 否則,呼叫memcpy(pch, &vch[nReadPos], nSize)複製nSize個字元,從vch的nReadPos位置開始,到由pch指向的一段預先分配的記憶體。接著從nReadPos向前移至下一個起始位置nReadPosNext(第22行)。
該實現表明1)當一段資料被從流中讀取之後,該段資料無法被再次讀取;2)nReadPos是第一個有效資料的讀取位置。
CDataStream::write()非常簡單。它將由pch指向的nSize個字元附加到vch的結尾。
巨集READDATA()和WRITEDATA()
函式CDataStream::read()與CDataStream::write()的作用是序列化/反序列化原始型別(int,bool,unsigned long等)。為了序列化這些資料型別,這些型別的指標將被轉換為char*。由於這些型別的大小目前已知,它們可以從CDataStream中讀取或者寫入至字元緩衝。兩個用於引用這些函式的巨集被定義為助手。
#define WRITEDATA(s, obj) s.write((char*)&(obj), sizeof(obj)) #define READDATA(s, obj) s.read((char*)&(obj), sizeof(obj))
這裡是如何使用這些巨集的例子。下面的函式將序列化一個unsigned long型別。
template<typename Stream> inline void Serialize(Stream& s, unsigned long a, int, int=0) { WRITEDATA(s, a); }
把WRITEDATA(s, a)用自身的定義取代,以下是展開以後的函式:
template<typename Stream> inline void Serialize(Stream& s, unsigned long a, int, int=0) { s.write((char*)&(a), sizeof(a)); }
該函式接受一個unsigned long引數a,獲取它的記憶體地址,轉換指標為char*並呼叫函式s.write()。
CDataStream中的操作符 << 和 >>
CDataStream過載了操作符<< 和 >>用於序列化和反序列化。
template<typename T> CDataStream& operator<<(const T& obj) { // Serialize to this stream ::Serialize(*this, obj, nType, nVersion); return (*this); } template<typename T> CDataStream& operator>>(T& obj) { // Unserialize from this stream ::Unserialize(*this, obj, nType, nVersion); return (*this); }
標頭檔案serialize.h包含了14個過載後的這兩個全域性函式給14個原始型別(signed和unsigned版本char,short,int,long和long long,以及char,float,double和bool)以及6個過載版本的6個複合型別(string,vector,pair,map,set和CScript)。因此,對於這些型別,你可以簡單地使用以下程式碼來序列化/反序列化資料:
CDataStream ss(SER_GETHASH); ss<<obj1<<obj2; //序列化 ss>>obj3>>obj4; //反序列化
如果沒有任何實現的型別符合第二個引數obj,則以下泛型T全域性函式將會被呼叫。
template<typename Stream, typename T> inline void Serialize(Stream& os, const T& a, long nType, int nVersion=VERSION) { a.Serialize(os, (int)nType, nVersion); }
對於該泛型版本,型別T應該用於實現一個成員函式和簽名T::Serialize(Stream, int, int)。它將通過a.Serialize()被呼叫。
怎樣實現一個型別的序列化
在之前的介紹當中,泛型T需要實現以下三個成員函式進行序列化。
unsigned int GetSerializeSize(int nType=0, int nVersion=VERSION) const; void Serialize(Stream& s, int nType=0, int nVersion=VERSION) const; void Unserialize(Stream& s, int nType=0, int nVersion=VERSION);
這三個函式將由它們相對應的帶泛型T的全域性函式呼叫。這些全域性函式則由CDataStream中過載的操作符<<和>>呼叫。
一個巨集IMPLEMENT_SERIALIZE(statements)用於定義任意型別的這三個函式的實現。
#define IMPLEMENT_SERIALIZE(statements) \ unsigned int GetSerializeSize(int nType=0, int nVersion=VERSION) const \ { \ CSerActionGetSerializeSize ser_action; \ const bool fGetSize = true; \ const bool fWrite = false; \ const bool fRead = false; \ unsigned int nSerSize = 0; \ ser_streamplaceholder s; \ s.nType = nType; \ s.nVersion = nVersion; \ {statements} \ return nSerSize; \ } \ template<typename Stream> \ void Serialize(Stream& s, int nType=0, int nVersion=VERSION) const \ { \ CSerActionSerialize ser_action; \ const bool fGetSize = false; \ const bool fWrite = true; \ const bool fRead = false; \ unsigned int nSerSize = 0; \ {statements} \ } \ template<typename Stream> \ void Unserialize(Stream& s, int nType=0, int nVersion=VERSION) \ { \ CSerActionUnserialize ser_action; \ const bool fGetSize = false; \ const bool fWrite = false; \ const bool fRead = true; \ unsigned int nSerSize = 0; \ {statements} \ }
以下例子示範怎樣使用該巨集。
#include <iostream> #include "serialize.h" using namespace std; class AClass { public: AClass(int xin) : x(xin){}; int x; IMPLEMENT_SERIALIZE(READWRITE(this->x);) } int main() { CDataStream astream2; AClass aObj(200); //一個x為200的AClass型別物件 cout<<"aObj="<<aObj.x>>endl; asream2<<aObj; AClass a2(1); //另一個x為1的物件 astream2>>a2 cout<<"a2="<<a2.x<<endl; return 0; }
這段程式序列化/反序列化AClass物件。它將在螢幕上輸出下面的結果。
aObj=200 a2=200
AClass的這三個序列化/反序列化成員函式可以在一行程式碼中實現:
IMPLEMENT_SERIALIZE(READWRITE(this->x);)
巨集READWRITE()的定義如下
#define READWRITE(obj) (nSerSize += ::SerReadWrite(s, (obj), nType, nVersion, ser_action))
該巨集的展開被放在巨集IMPLEMENT_SERIALIZE(statements)的全部三個函式裡。因此,它一次需要完成三件事情:1)返回序列化後資料的大小,2)序列化(寫入)資料至流;3)從流中反序列化(讀取)資料。參考巨集IMPLEMENT_SERIALIZE(statements)中對這三個函式的定義。
想要了解巨集READWRITE(obj)怎樣工作,你首先需要明白它的完整形式當中的nSerSize,s,nType,nVersion和ser_action是怎麼來的。它們全部來自巨集IMPLEMENT_SERIALIZE(statements)的三個函式主體部分:
- nSerSize是一個unsigned int,在三個函式當中初始化為0;
- ser_action是一個物件在三個函式當中均有宣告,但為三種不同型別。它在三個函式當中分別為CSerActionGetSerializeSize、CSerActionSerialize和CSerActionUnserialize;
- s在第一個函式中定義為ser_streamplaceholder型別。它是第一個傳入至另外兩個函式的引數,擁有引數型別Stream;
- nType和nVersion在三個函式中均為傳入引數。
因此,一旦巨集READWRITE()擴充套件至巨集IMPLEMENT_SERIALIZE(),所有它的符號都將被計算,因為它們已經存在於巨集IMPLEMENT_SERIALIZE()的主體中。READWRITE(obj)的擴充套件呼叫一個全域性函式::SerReadWrite(s, (obj), nType, nVersion, ser_action)。這裡是這個函式的全部三種版本。
template<typename Stream, typename T> inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionGetSerializeSize ser_action) { return ::GetSerializeSize(obj, nType, nVersion); } template<typename Stream, typename T> inline unsigned int SerReadWrite(Stream& s, const T& obj, int nType, int nVersion, CSerActionSerialize ser_action) { ::Serialize(s, obj, nType, nVersion); return 0; } template<typename Stream, typename T> inline unsigned int SerReadWrite(Stream& s, T& obj, int nType, int nVersion, CSerActionUnserialize ser_action) { ::Unserialize(s, obj, nType, nVersion); return 0; }
如你所見,函式::SerReadWrite()被過載為三種版本。取決於最後一個引數,它將會調分別用全域性函式::GetSerialize(),::Serialize()和::Unserialize();這三個函式在前面章節已經介紹。
如果你檢查三種不同版本的::SerReadWrite()的最後一個引數,你會發現它們全部為空型別。這三種類型的唯一用途是區別::SerReadWrite()的三個版本,繼而被巨集IMPLEMENT_SERIALIZE()定義的所有函式使用。