我們知道,比特幣網(wǎng)絡(luò)上的交易需要由節(jié)點(diǎn)驗(yàn)證并傳播,那么當(dāng)節(jié)點(diǎn)收到一筆交易時(shí),到底做了哪些處理?具體的流程又是怎樣?使用了什么樣的數(shù)據(jù)結(jié)構(gòu)?網(wǎng)絡(luò)上的文章大多是泛泛而談,讀者看的云里霧里,難以理出清晰的脈絡(luò)。本文旨在解決讀者的疑惑,通過剖析比特幣的源碼,勾勒出節(jié)點(diǎn)處理交易時(shí)的完整流程圖。
注1:本文的分析以中本聰?shù)拇av0.1.15版本為藍(lán)本,節(jié)點(diǎn)指代為全節(jié)點(diǎn)。
注2:如果對(duì)比特幣交易的一些基本概念還不清楚,請(qǐng)先閱讀MasteringBitcoin這本書的相應(yīng)章節(jié),此書中文版叫做《精通比特幣》。
注3:文中提到的關(guān)鍵步驟會(huì)貼出相應(yīng)源碼,非關(guān)鍵部分請(qǐng)參考流程圖自行去看源碼
注4:文中提到的交易處理流程針對(duì)單筆交易(loose transaction)
1. 約定術(shù)語
為避免混淆,首先對(duì)一些容易產(chǎn)生歧義的概念作出約定,先來看一筆交易的結(jié)構(gòu)。比特幣的交易結(jié)構(gòu)采用UTXO模型,交易主要由輸入輸出兩個(gè)數(shù)組構(gòu)成。
{
"version": 1,
"locktime": 0,
"vin": [
{
"txid":"7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
"vout": 0,
"scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.01500000,
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value": 0.08450000,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
}
]
}
引用:我們說此筆交易引用了txid為7957....6f18的交易。
子交易: 此筆交易作為引用者,可以稱作子交易。
父交易: txid為7957....6f18的交易作為被引用者,可以稱作父交易。
孤兒交易: 找不到父交易的子交易,比如在交易池或本地區(qū)塊鏈中找不到txid為7957....6f18的交易,我們就稱子交易為孤兒交易。
交易池:內(nèi)存的一塊區(qū)域,存放已經(jīng)通過驗(yàn)證待被打包進(jìn)入?yún)^(qū)塊鏈的交易。
孤兒交易池: 內(nèi)存的一塊區(qū)域,存放通過驗(yàn)證但找不到父交易的孤兒交易。
注意:交易的結(jié)構(gòu)是多輸入多輸出的,所以父交易對(duì)應(yīng)多個(gè)子交易,子交易也對(duì)應(yīng)多個(gè)父交易,這與通常的父子概念不同,只是代表了引用和被引用的關(guān)系
2. 關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)
2.1 關(guān)鍵類
首先是交易類,nVersion和nLockTime涉及到腳本的知識(shí),會(huì)在后面的文章提及。交易類的主要內(nèi)容是輸入輸出數(shù)組,輸入是CTxIn對(duì)象的集合,輸出是CTxOut對(duì)象的集合。
class CTransaction
{
public:
int nVersion;
vector<CTxIn> vin; //輸入數(shù)組
vector<CTxOut> vout; //輸出數(shù)組
int nLockTime;
/*成員函數(shù)略*/
}
CTxIn類是輸入的結(jié)構(gòu),由CoutPoint、解鎖腳本和nSequence構(gòu)成。nSequence字段默認(rèn)填最大值,在替換交易時(shí)有用,這里不做過多的解釋。
class CTxIn
{
public:
COutPoint prevout; //被引用交易的hash和vout索引
CScript scriptSig; //解鎖腳本
unsigned int nSequence;
/*成員函數(shù)略*/
}
CoutPoint類保存父交易的hash值和vout數(shù)組的索引,我們看到交易輸入的重要部分其實(shí)就是父交易的輸出。
class COutPoint
{
public:
uint256 hash; //交易hash,即txid
unsigned int n; //輸出索引
/*成員函數(shù)略*/
}
CTxOut類是輸出的結(jié)構(gòu),由交易額和鎖定腳本構(gòu)成。
class CTxOut
{
public:
int64 nValue; //交易額
CScript scriptPubKey; //鎖定腳本
/*成員函數(shù)略*/
}
CInPoint類是相對(duì)CoutPoint出現(xiàn)的一個(gè)類,存的是子交易的指針和輸入數(shù)組的索引
class CInPoint
{
public:
CTransaction* ptx; //子交易指針
unsigned int n; //數(shù)組vin索引
/*成員函數(shù)略*/
}
CTxIndex類的作用是保存交易在硬盤中的位置,他還可以記錄交易的輸出中哪些是已經(jīng)被花掉的,這在雙花檢查中非常重要。
class CTxIndex
{
public:
CDiskTxPos pos; //交易在硬盤中的位置
vector<CDiskTxPos> vSpent; //此筆交易的輸出被花費(fèi)標(biāo)志位
}
2.2 關(guān)鍵全局變量
?交易池
交易池的數(shù)據(jù)結(jié)構(gòu)由兩個(gè)全局map構(gòu)成。mapTransaction的key是交易的hash值,value是交易本身,這個(gè)map是交易池的主要結(jié)構(gòu)。mapNextTx以COutPoint為key,以CinPoint為value,存的是父子交易的聯(lián)系,在檢測(cè)內(nèi)存沖突時(shí)有用,是交易池的輔助結(jié)構(gòu)。
map<uint256, CTransaction> mapTransactions; //key: 交易hash value:交易
map<COutPoint, CInPoint> mapNextTx; //key: 父交易的hash和vout索引 value:子交易的指針和vin索引
?孤兒交易池
孤兒交易池的數(shù)據(jù)結(jié)構(gòu)也由兩個(gè)全局map構(gòu)成,但和交易池略有不同。首先value存的是交易指針,交易在堆中保存,另外mapOrphanTransactionsByPrev是一個(gè)multimap,因?yàn)橐还P交易有多個(gè)輸出,可對(duì)應(yīng)多筆孤兒交易。
map<uint256, CDataStream*> mapOrphanTransactions; //key: 孤兒交易的hash value:孤兒交易的指針
multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; //key: 父交易的hash value:孤兒交易的指針
3. 總體流程
下面介紹節(jié)點(diǎn)收到單筆交易(loose transaction)并進(jìn)行處理的整體流程,先貼出流程圖

接下來對(duì)流程圖中的重要部分做出說明
?交易驗(yàn)證
處理一筆交易時(shí)首先要對(duì)交易進(jìn)行驗(yàn)證,判斷能否被放入交易池。驗(yàn)證交易的函數(shù)是AcceptTransaction。驗(yàn)證交易的流程會(huì)在后面單獨(dú)展開,這里先提一下。
bool AcceptTransaction(CTxDB& txdb, bool fCheckInputs=true, bool* pfMissingInputs=NULL);
bool AcceptTransaction(bool fCheckInputs=true, bool* pfMissingInputs=NULL)
{
CTxDB txdb("r");
//調(diào)用重載的另一個(gè)AcceptTransaction,第一個(gè)參數(shù)是本地?cái)?shù)據(jù)庫blkindex.dat
return AcceptTransaction(txdb, fCheckInputs, pfMissingInputs);
}
?加入孤兒交易池
我們看到,AcceptTransaction函數(shù)有一個(gè)傳出參數(shù)是pfMissingInput, 如果交易未能通過驗(yàn)證,且原因是在區(qū)塊鏈和交易池中找不到這筆交易的所有父交易,那么傳出參數(shù)將會(huì)被置True,交易會(huì)被放入孤兒交易池。這里貼出交易加入孤兒池的代碼
void AddOrphanTx(const CDataStream& vMsg)
{
CTransaction tx;
CDataStream(vMsg) >> tx;
uint256 hash = tx.GetHash();
if (mapOrphanTransactions.count(hash))
return;
CDataStream* pvMsg = mapOrphanTransactions[hash] = new CDataStream(vMsg);
//孤兒交易的所有父交易,都要和孤兒交易組成鍵值對(duì),存入multimap
foreach(const CTxIn& txin, tx.vin)
mapOrphanTransactionsByPrev.insert(make_pair(txin.prevout.hash, pvMsg));
}
?加入錢包,廣播交易
如果交易通過驗(yàn)證,判斷交易的輸出中是否有發(fā)給本節(jié)點(diǎn)的(看交易的鎖定腳本的公鑰hash),如果有就存入錢包,然后向其他節(jié)點(diǎn)轉(zhuǎn)播交易,這涉及到錢包和p2p網(wǎng)絡(luò)的知識(shí),在本篇文章中不會(huì)分析這個(gè)子流程。
?排查孤兒交易池
繼續(xù)向下看,交易hash值被放入一個(gè)名叫vWorkQueue的vector中,這個(gè)vector保存通過驗(yàn)證的交易。我們需要遍歷這個(gè)vector,處理此交易對(duì)應(yīng)的孤兒交易集合。首先獲得這個(gè)孤兒交易集合,然后遍歷集合,對(duì)集合的每筆交易進(jìn)行交易驗(yàn)證(這里驗(yàn)證函數(shù)的傳出參數(shù)pfMissingInputs不生效,因?yàn)榻灰滓言诠聝航灰壮兀?,如果?yàn)證通過就加入vWorkQueue。這樣形成了遞歸,凡不再是孤兒交易的交易都會(huì)被從孤兒交易池中撈出來,放在vWorkQueue中。我們從流程圖下方中的兩個(gè)循環(huán)中可以清楚地看到這個(gè)流程,這塊的代碼也貼出來
vWorkQueue.push_back(inv.hash);
// Recursively process any orphan transactions that depended on this one
//遞歸處理與此筆交易有關(guān)的孤兒交易
for (int i = 0; i < vWorkQueue.size(); i++)
{
//此筆交易的hash,對(duì)于孤兒交易是父交易
uint256 hashPrev = vWorkQueue[i];
//遍歷這筆交易對(duì)應(yīng)的孤兒交易
for (multimap<uint256, CDataStream*>::iterator mi = mapOrphanTransactionsByPrev.lower_bound(hashPrev);
mi != mapOrphanTransactionsByPrev.upper_bound(hashPrev);
++mi)
{
const CDataStream& vMsg = *((*mi).second);
CTransaction tx;
CDataStream(vMsg) >> tx;
CInv inv(MSG_TX, tx.GetHash());
//孤兒交易找到一個(gè)父交易, 對(duì)孤兒交易重新調(diào)用AcceptTransaction函數(shù),此時(shí)不用填fMissingInput參數(shù),因?yàn)楣聝航灰滓呀?jīng)在孤兒池中
if (tx.AcceptTransaction(true))
{
printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
AddToWalletIfMine(tx, NULL);
RelayMessage(inv, vMsg);
mapAlreadyAskedFor.erase(inv);
//接受了的孤兒交易也放到vWorkQueque中,因此有了遞歸
vWorkQueue.push_back(inv.hash);
}
}
}
? 整理孤兒交易池
最后一步好理解, 孤兒交易池中能撈出來的已經(jīng)全被撈出來了,對(duì)照vWorkQueue整理孤兒交易池的兩個(gè)map就好了,這里代碼就不貼出了。
4. 驗(yàn)證交易步驟
在總體流程中為了保持脈絡(luò)清晰沒有展開說驗(yàn)證交易的步驟,實(shí)際上驗(yàn)證交易需要很多步驟,先看流程圖

從圖中可以看到,驗(yàn)證交易分六個(gè)大步驟:
?coinbase檢查
如果收到的交易是coinbase交易,那么驗(yàn)證失敗,因?yàn)閏oinbase交易只能出現(xiàn)在區(qū)塊中,在網(wǎng)絡(luò)上傳播的單筆交易一定是錯(cuò)誤的。
?常規(guī)檢查
常規(guī)檢查會(huì)查看輸入輸出是否為空,查看交易額是否為負(fù)值,輸入來源是否為null。
?交易是否已經(jīng)存在
從交易池、區(qū)塊鏈賬本中查找此筆交易是否已經(jīng)存在。對(duì)于交易池,直接查找mapTransactions就可以了。對(duì)于區(qū)塊鏈賬本,需要查找數(shù)據(jù)庫,對(duì)應(yīng)的數(shù)據(jù)庫文件是blkindex.dat。
?沖突檢查
這是檢查交易池的一個(gè)關(guān)鍵步驟,如果這筆交易與交易池中的某筆交易所引用的交易是一模一樣的,那么可以認(rèn)定這兩筆交易是一樣的,只是新舊不同(通過nSequence比較),會(huì)用新交易替代舊交易;如果這筆交易與交易池中的某筆交易所引用的交易只是部分相同,這種情況就屬于沖突,后收到的交易一定會(huì)被節(jié)點(diǎn)拒絕。此處的代碼初看時(shí)有點(diǎn)令人費(fèi)解,網(wǎng)上有些源碼分析的解釋是錯(cuò)誤的。貼出源碼:
// Check for conflicts with in-memory transactions
//檢查與內(nèi)存中其他交易的沖突, 只有在內(nèi)存里找到與當(dāng)前這筆交易引用的輸出是一模一樣的交易,才比較新舊
CTransaction* ptxOld = NULL;
for (int i = 0; i < vin.size(); i++)
{
//獲得這筆交易的來源,就是父交易的hash和out索引
COutPoint outpoint = vin[i].prevout;
if (mapNextTx.count(outpoint))
{
// Allow replacing with a newer version of the same transaction
//必須是所有的outpoint都滿足,非0,代表第一個(gè)沒滿足條件,就可以直接return false了
if (i != 0)
return false;
ptxOld = mapNextTx[outpoint].ptx;
//當(dāng)前交易如果不比舊的新,return false,誰有最大的nSequence誰更新
if (!IsNewerThan(*ptxOld))
return false;
//確保ptxOld和新交易是同一筆交易
for (int i = 0; i < vin.size(); i++)
{
COutPoint outpoint = vin[i].prevout;
if (!mapNextTx.count(outpoint) || mapNextTx[outpoint].ptx != ptxOld)
return false;
}
break;
}
}
?輸入檢查
沖突檢查檢查的是與交易池中交易的沖突,輸入檢查檢查的則是與區(qū)塊鏈中交易的沖突,這個(gè)流程比較復(fù)雜,會(huì)在后面單獨(dú)展開。
?加入交易池
如果上面5步的檢查都通過了,剩下的工作就是把交易放入交易池了。如果上面的沖突檢查中ptxOld不為null, 還要把舊的交易從交易池中移除。很多人覺得交易池很神秘,其實(shí)一點(diǎn)也不神秘。貼出將交易添加到交易池的代碼,只有幾行
bool CTransaction::AddToMemoryPool()
{
// Add to memory pool without checking anything. Don't call this directly,
// call AcceptTransaction to properly check the transaction first.
CRITICAL_BLOCK(cs_mapTransactions)
{
uint256 hash = GetHash();
mapTransactions[hash] = *this;
for (int i = 0; i < vin.size(); i++)
mapNextTx[vin[i].prevout] = CInPoint(&mapTransactions[hash], i);
nTransactionsUpdated++;
}
return true;
}
5. 輸入檢查
下面展開分析輸入檢查的流程,這是檢查流程中的重要部分,包含了驗(yàn)證簽名和雙花檢查兩個(gè)核心步驟,先貼出流程圖

下面針對(duì)重要的部分展開分析
?查找父交易位置
我們從圖中看到,首先遍歷交易的輸入序列,然后在本地區(qū)塊鏈中查找父交易的位置,如果找不到就在內(nèi)存中找,在內(nèi)存中還找不到的話,就會(huì)將*pfMissInputs置true, 可以看出只要有一個(gè)父交易找不到子交易就被認(rèn)為是孤兒交易。針對(duì)找到的情況,用一個(gè)CTxIndex類型的局部變量txIndex保存父交易的位置。如果父交易在區(qū)塊鏈中,則位置信息有數(shù)據(jù),父交易在交易池中,位置信息無數(shù)據(jù)。
?父交易coinbase進(jìn)行檢查
現(xiàn)在我們知道了父交易的位置信息,接下來用一個(gè)局部變量txPrev保存父交易,由于coinbase交易必須要等100個(gè)確認(rèn)后礦工的比特幣才能被花掉,因此如果父交易是coinbase交易的話,滿足100個(gè)確認(rèn)才能驗(yàn)證(比較區(qū)塊鏈高度)通過.
?驗(yàn)證簽名
下面進(jìn)行驗(yàn)證簽名檢查,驗(yàn)證簽名涉及腳本和橢圓曲線加密算法的知識(shí),其原理會(huì)在后續(xù)的文章展開討論。這里講下流程,將子交易的解鎖腳本和父交易的鎖定腳本連起來,然后調(diào)用腳本執(zhí)行函數(shù),執(zhí)行過程類似于壓棧彈棧操作。執(zhí)行完腳本后,如果子交易的發(fā)起者擁有父交易鎖定腳本中公鑰hash對(duì)應(yīng)的私鑰,那么解鎖腳本會(huì)解開鎖定腳本,函數(shù)會(huì)返回True, 貼出驗(yàn)證簽名的代碼
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);
}
?檢查雙花
我們既然拿到了父交易的位置信息,就可以查看這個(gè)結(jié)構(gòu)體的vSpent部分,如果txIndex.vSpent[prevout.n]已經(jīng)被置位了,說明父交易的這個(gè)輸出已經(jīng)被花掉了,子交易的輸入用了已經(jīng)被花費(fèi)掉的輸出,這就產(chǎn)生了“雙花”(double spend)。因此,驗(yàn)證不會(huì)通過。
?標(biāo)志位置位
雙花檢查通過,接下來將txIndex.vSpent[prevout.n]置位,由于我們驗(yàn)證的是單筆交易(loose trsanction), 這筆交易并沒有入塊,所以這個(gè)標(biāo)志位只要被置位即可,內(nèi)容沒有要求,位置也不會(huì)被保存。如果驗(yàn)證的是塊中交易,這個(gè)標(biāo)志位要填塊的位置,并且這個(gè)位置會(huì)被寫入數(shù)據(jù)庫。
?計(jì)算手續(xù)費(fèi)
遍歷完所有父交易后,我們可以累加得到輸入的總金額,用這個(gè)金額減去輸出總金額,就得到了當(dāng)前交易的手續(xù)費(fèi),如果手續(xù)費(fèi)大于最小手續(xù)費(fèi),可以驗(yàn)證通過。
此處貼出輸入檢查的源碼
bool CTransaction::ConnectInputs(CTxDB& txdb, map<uint256, CTxIndex>& mapTestPool, CDiskTxPos posThisTx, int nHeight, int64& nFees, bool fBlock, bool fMiner, int64 nMinFee)
{
// Take over previous transactions' spent pointers
if (!IsCoinBase())
{
int64 nValueIn = 0;
for (int i = 0; i < vin.size(); i++)
{
COutPoint prevout = vin[i].prevout;
// Read txindex
CTxIndex txindex;
bool fFound = true;
if (fMiner && mapTestPool.count(prevout.hash))
{
// Get txindex from current proposed changes
txindex = mapTestPool[prevout.hash];
}
else
{
// Read txindex from txdb
//查引用的交易在硬盤中的位置,把位置存到txindex中
fFound = txdb.ReadTxIndex(prevout.hash, txindex);
}
if (!fFound && (fBlock || fMiner))
return fMiner ? false : error("ConnectInputs() : %s prev tx %s index entry not found", GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str());
// Read txPrev
CTransaction txPrev;
//在硬盤里沒查到引用的這筆交易
if (!fFound || txindex.pos == CDiskTxPos(1,1,1))
{
// Get prev tx from single transactions in memory
CRITICAL_BLOCK(cs_mapTransactions)
{
//在內(nèi)存池(mapTransaction)里查引用的這筆交易
if (!mapTransactions.count(prevout.hash))
return error("ConnectInputs() : %s mapTransactions prev not found %s", GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str());
txPrev = mapTransactions[prevout.hash];
}
if (!fFound)
txindex.vSpent.resize(txPrev.vout.size());
}
else
{
// Get prev tx from disk
if (!txPrev.ReadFromDisk(txindex.pos))
return error("ConnectInputs() : %s ReadFromDisk prev tx %s failed", GetHash().ToString().substr(0,6).c_str(), prevout.hash.ToString().substr(0,6).c_str());
}
if (prevout.n >= txPrev.vout.size() || prevout.n >= txindex.vSpent.size())
return error("ConnectInputs() : %s prevout.n out of range %d %d %d", GetHash().ToString().substr(0,6).c_str(), prevout.n, txPrev.vout.size(), txindex.vSpent.size());
// If prev is coinbase, check that it's matured
if (txPrev.IsCoinBase())
for (CBlockIndex* pindex = pindexBest; pindex && nBestHeight - pindex->nHeight < COINBASE_MATURITY-1; pindex = pindex->pprev)
if (pindex->nBlockPos == txindex.pos.nBlockPos && pindex->nFile == txindex.pos.nFile)
return error("ConnectInputs() : tried to spend coinbase at depth %d", nBestHeight - pindex->nHeight);
// Verify signature
if (!VerifySignature(txPrev, *this, i))
return error("ConnectInputs() : %s VerifySignature failed", GetHash().ToString().substr(0,6).c_str());
// Check for conflicts
if (!txindex.vSpent[prevout.n].IsNull())
return fMiner ? false : error("ConnectInputs() : %s prev tx already used at %s", GetHash().ToString().substr(0,6).c_str(), txindex.vSpent[prevout.n].ToString().c_str());
// Mark outpoints as spent
txindex.vSpent[prevout.n] = posThisTx;
// Write back
if (fBlock)
txdb.UpdateTxIndex(prevout.hash, txindex);
else if (fMiner)
mapTestPool[prevout.hash] = txindex;
nValueIn += txPrev.vout[prevout.n].nValue;
}
// Tally transaction fees
int64 nTxFee = nValueIn - GetValueOut();
if (nTxFee < 0)
return error("ConnectInputs() : %s nTxFee < 0", GetHash().ToString().substr(0,6).c_str());
if (nTxFee < nMinFee)
return false;
nFees += nTxFee;
}
if (fBlock)
{
// Add transaction to disk index
if (!txdb.AddTxIndex(*this, posThisTx, nHeight))
return error("ConnectInputs() : AddTxPos failed");
}
else if (fMiner)
{
// Add transaction to test pool
mapTestPool[GetHash()] = CTxIndex(CDiskTxPos(1,1,1), vout.size());
}
return true;
}
至此,全節(jié)點(diǎn)處理交易的流程就理清了,讀者最好對(duì)照流程圖去閱讀一遍源碼,這樣會(huì)理解的更加透徹。
BTech原創(chuàng),未經(jīng)許可不得轉(zhuǎn)載