關于交易部分可以先閱讀《精通比特幣》第五章
本文內容參考自https://blog.csdn.net/g2com/article/details/64386251
對于初次分析比特幣源代碼,建議先閱讀最原始版本的比特幣源代碼original-bitcoin。此版本源代碼比較簡單,可以幫助快速理解比特幣各個階段的工作流程及原理。
1.SendMoney()
當比特幣客戶端向某個地址發送比特幣時,便會調用該函數。函數位于'src/main.cpp'第2625行。
bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew)
{
CRITICAL_BLOCK(cs_main)
{
int64 nFeeRequired;
if (!CreateTransaction(scriptPubKey, nValue, wtxNew, nFeeRequired))
{
strig 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;
}
該方法包含三個參數:
- scriptPubKey為收款人公鑰鎖定腳本,關于鎖定腳本和解鎖腳本將會在下章做分析。
- nValue表示將要轉賬的金額。該金額并未包含交易費nTrasactionFee。
- wtxNew是一個CWalletTx類的本地變量。該變量目前的值為空,之后會包含若干CMerkleTX類對象。該類由CTransaction衍生而來,并且添加了若干方法。我們暫時先不管具體細節,僅將其看作CTransaction類。
SendMoney()
首先調用了CreateTransaction()
函數,這個函數作用便是構造一筆新的交易,也是本文重點分析的函數。該函數源代碼如下:
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即腳本代碼
- nValue是將要轉賬的數額,交易費nTransactionFee并未包括在內。
- wtxNew是一個新的Tx實例。
- nFeeRequiredRet是一筆用來支付交易費的輸出交易,在該方法執行完成之后獲得。
函數首先對實例wtxNew初始化,隨后計算總共費用nValue=轉賬金額+交易費,調用 SelectCoin()
尋找合適的交易輸入。
實際上,并不存在儲存比特幣地址或賬戶余額的地點,只有被所有者鎖住的、分散的UTXO。“一個用戶的比特幣余額”,這個概念是一個通過比特幣錢包應用創建的派生之物。比特幣錢包通過掃描區塊鏈并聚合所有屬于該用戶的UTXO來計算該用戶的余額。
bool SelectCoins(int64 nTargetValue, set<CWalletTx*>& setCoinsRet)
{
setCoinsRet.clear();
// List of values less than target
int64 nLowestLarger = _I64_MAX;
CWalletTx* pcoinLowestLarger = NULL;
vector<pair<int64, CWalletTx*> > vValue;
int64 nTotalLower = 0;
...
}
我們知道比特幣是基于UTXO模型的,所以SelectCoin便負責從所有屬于該用戶的UTXO中找到一組符合轉賬金額的輸入。具體的尋找算法此處便不具體分析。
在得到一組輸入之后會計算所有輸入的總金額nValueIn,一般輸入總金額是大于轉賬金額的,所以后面會構造一筆轉給自己地址的輸出,用于找零。
隨后調用wtxNew.vout.push_back(CTxOut(nValueOut, scriptPubKey))
構造第一筆輸出,指向該筆交易的轉賬地址。關于CTxIn和CTxOut的數據結構可以參考https://blog.csdn.net/pure_lady/article/details/77771392
如果需要找零(nValueIn > nValue),添加另一筆輸出交易至wtxNew并將零錢發回本人。該過程包含以下步驟:
從setCoin當中獲取第一筆交易txFirst,依次檢查txFirst.vout中的輸出是否屬于本人。如果是則從該筆輸出交易當中提取出公鑰ExtractPubKey
,并放入本地變量vchPubKey
將vchPubKey放入腳本vchPubKey OP_CHECKSIG,并使用這段腳本代碼為wtxNew添加一個支付給本人的輸出交易。
因為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的輸入交易位置。
對于交易簽名函數SignSignature
,以下為源代碼:
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文件里,后兩個參數已默認給出。
SignSignature(*pcoin, wtxNew, nIn++)
- txFrom是一個*pcoin對象。即我們前面找到的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()
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());
}
SignatureHash(scriptPrereq + txout.scriptPubKey, txTo, nIn, nHashType);
以下是該函數所需要的參數:
- 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創建者的公鑰,也是你自己的公鑰。
在了解了輸入交易之后,我們來一起了解SignatureHash()是怎樣工作的。
SignatureHash()首先將txTO拷貝至txTmp,接著清空txTmp.vin中每一筆輸入交易的scriptSig,除了txTmp.vin[nIn]之外,該輸入交易的scriptSig被設為scriptCode(第14、15行)。
接著,該函數檢驗nHashType的值。根據不同的nHAshType選擇不同的置空操作。
- SIGHASH_ALL是默認選項,具體流程是把所有的TxOut都納入臨時Tx中用來生成被簽署的交易,相當于針對這個TxIn,這個交易中的所有的TxOut都已經被這個TxIn承認,不可改
- SIGHASH_NONE,具體流程是把所有的TxOut都置空,相當于針對這個TxIn,不關心這個交易的TxOut是什么情況,即使被替換了也是可以的
- SIGHASH_SINGLE,具體流程是只保留和自已同樣index的out,其他的out都置空,表示只關心和自己同樣index的out,其他的out不關心。比如當前的txin是這個交易的第3個in(index=2),那么這個交易的第3個out保留,其他的out都置空。
- SIGHASH_ANYONECANPAY比較特殊,他是獨立的??梢院土?個標志取并集。它表示簽署這個TxIn的時候我連其他的TxIn都不關心,可以和前面3個并存。
在最后4行代碼中,txTmp和nHashType變成序列化后的類型CDataStream對象。該類型包括一個裝有數據的字符容器類型。所返回的哈希值是Hash()方法在計算序列化后的數據所得到的。
到這里我們便生成了txOut的哈希值hash,接下來會調用Solver()函數簽署剛才生成hash。
Solver()
Solver(txout.scriptPubKey, hash, nHashType, txin.scriptSig)
其源代碼如下:
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將存放該函數返回的簽名。
該函數首先將scriptSigRet清空,隨后調用Solver(scriptPubKey, vSolution)
,此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,則將模板A中的OP_PUBKEYHASH與腳本A中的<你的地址160位哈希>配對,并將該對放入vSolutionRet。
如果輸入腳本為腳本B,則從模板B中提取運算符OP_PUBKEY,和從腳本B中提取運算元<你的公鑰>,將二者配對并放入vSolutionRet。
如果輸入腳本與兩個模板均不匹配,則返回false。
回到有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()
最后將調用EvalScript()來運行一小段腳本并檢查簽名是否合法。
EvalScript(txin.scriptSig + CScript(OP_CODESEPARATOR) + txout.scriptPubKey, txTo, nIn)
其源代碼如下:
bool EvalScript(const CScript& script, const CTransaction& txTo, unsigned int nIn, int nHashType,
vector<vector<unsigned char> >* pvStackRet)
{
CAutoBN_CTX pctx;
CScript::const_iterator pc = script.begin();
CScript::const_iterator pend = script.end();
CScript::const_iterator pbegincodehash = script.begin();
vector<bool> vfExec;
vector<valtype> stack;
vector<valtype> altstack;
if (pvStackRet)
pvStackRet->clear();
while (pc < pend)
{
bool fExec = !count(vfExec.begin(), vfExec.end(), false);
...
}
if (pvStackRet)
*pvStackRet = stack;
return (stack.empty() ? false : CastToBool(stack.back()));
}
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輸入交易列表中的位置。
該函數將根據你輸入的腳本,依次取出腳本中的操作代碼進行相應操作,并對最后的模擬執行結果做判斷,返回執行結果。如果結果為true,則完成了SignSignature()
,此時便生成了一筆新的交易。
回到SendMoney()
生成了一筆新的交易后,利用函數CommitTransactionSpent(wtxNet)
嘗試將這筆交易提交至數據庫,之后判斷交易是否提交成功,如果該筆交易提交成功wtxNew.AcceptTransaction()=true
,將這筆交易廣播至其他peer節點wtxNew.RelayWalletTransaction()
。
當礦工收到這筆交易的廣播之后會對交易進行相應操作,之后的章節我們將對新區塊的處理部分做詳細分析。
總結
比特幣生成一筆新的交易大致分為如下幾個階段:
- 根據轉賬金額以及交易費用從UTXO中尋找一組滿足條件的輸入。
- 對于這組輸入以及轉賬地址構造一個新的交易:輸入分別對應UTXO中的不同輸出,第一個輸出指向轉賬地址,如果有找零,則計算找零金額,將其放入第二個輸出,同時指向自身的錢包地址。
- 對構造好的輸入輸出分別進行簽名。
- 利用Solver函數對簽名完的交易做模擬運行,如果運行通過則生成完畢。
- 將交易提交至數據庫并廣播到其他節點。