比特幣全節(jié)點(diǎn)處理交易流程分析

我們知道,比特幣網(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)載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容