美文网首页图解区块链区块链
比特币全节点处理交易流程分析

比特币全节点处理交易流程分析

作者: BTech | 来源:发表于2018-08-15 16:18 被阅读0次

    我们知道,比特币网络上的交易需要由节点验证并传播,那么当节点收到一笔交易时,到底做了哪些处理?具体的流程又是怎样?使用了什么样的数据结构?网络上的文章大多是泛泛而谈,读者看的云里雾里,难以理出清晰的脉络。本文旨在解决读者的疑惑,通过剖析比特币的源码,勾勒出节点处理交易时的完整流程图。

    注1:本文的分析以中本聪的代码v0.1.15版本为蓝本,节点指代为全节点。
    注2:如果对比特币交易的一些基本概念还不清楚,请先阅读MasteringBitcoin这本书的相应章节,此书中文版叫做《精通比特币》。
    注3:文中提到的关键步骤会贴出相应源码,非关键部分请参考流程图自行去看源码
    注4:文中提到的交易处理流程针对单笔交易(loose transaction)

    1. 约定术语

    为避免混淆,首先对一些容易产生歧义的概念作出约定,先来看一笔交易的结构。比特币的交易结构采用UTXO模型,交易主要由输入输出两个数组构成。

    {
      "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的交易作为被引用者,可以称作父交易。

    孤儿交易: 找不到父交易的子交易,比如在交易池或本地区块链中找不到txid为7957....6f18的交易,我们就称子交易为孤儿交易。

    交易池:内存的一块区域,存放已经通过验证待被打包进入区块链的交易。

    孤儿交易池: 内存的一块区域,存放通过验证但找不到父交易的孤儿交易。

    注意:交易的结构是多输入多输出的,所以父交易对应多个子交易,子交易也对应多个父交易,这与通常的父子概念不同,只是代表了引用和被引用的关系

    2. 关键的数据结构

    2.1 关键类

    首先是交易类,nVersion和nLockTime涉及到脚本的知识,会在后面的文章提及。交易类的主要内容是输入输出数组,输入是CTxIn对象的集合,输出是CTxOut对象的集合。

    class CTransaction
    {
    public:
        int nVersion;
        vector<CTxIn> vin;   //输入数组
        vector<CTxOut> vout; //输出数组
        int nLockTime;
        /*成员函数略*/
    }
    

    CTxIn类是输入的结构,由CoutPoint、解锁脚本和nSequence构成。nSequence字段默认填最大值,在替换交易时有用,这里不做过多的解释。

    class CTxIn
    {
    public:
        COutPoint prevout; //被引用交易的hash和vout索引
        CScript scriptSig; //解锁脚本
        unsigned int nSequence;
        /*成员函数略*/
    }
    

    CoutPoint类保存父交易的hash值和vout数组的索引,我们看到交易输入的重要部分其实就是父交易的输出。

    class COutPoint
    {
    public:
        uint256 hash;   //交易hash,即txid
        unsigned int n; //输出索引
        /*成员函数略*/
    }
    

    CTxOut类是输出的结构,由交易额和锁定脚本构成。

    class CTxOut
    {
    public:
        int64 nValue;         //交易额
        CScript scriptPubKey; //锁定脚本
        /*成员函数略*/
    }
    

    CInPoint类是相对CoutPoint出现的一个类,存的是子交易的指针和输入数组的索引

    class CInPoint
    {
    public:
        CTransaction* ptx; //子交易指针
        unsigned int n;    //数组vin索引
        /*成员函数略*/
    }
    

    CTxIndex类的作用是保存交易在硬盘中的位置,他还可以记录交易的输出中哪些是已经被花掉的,这在双花检查中非常重要。

    class CTxIndex
    {
    public:
        CDiskTxPos pos;            //交易在硬盘中的位置
        vector<CDiskTxPos> vSpent; //此笔交易的输出被花费标志位
    }
    

    2.2 关键全局变量

    ❖交易池

    交易池的数据结构由两个全局map构成。mapTransaction的key是交易的hash值,value是交易本身,这个map是交易池的主要结构。mapNextTx以COutPoint为key,以CinPoint为value,存的是父子交易的联系,在检测内存冲突时有用,是交易池的辅助结构。

    map<uint256, CTransaction> mapTransactions; //key: 交易hash value:交易
    map<COutPoint, CInPoint>   mapNextTx; //key: 父交易的hash和vout索引 value:子交易的指针和vin索引
    
    ❖孤儿交易池

    孤儿交易池的数据结构也由两个全局map构成,但和交易池略有不同。首先value存的是交易指针,交易在堆中保存,另外mapOrphanTransactionsByPrev是一个multimap,因为一笔交易有多个输出,可对应多笔孤儿交易。

    map<uint256, CDataStream*> mapOrphanTransactions; //key: 孤儿交易的hash value:孤儿交易的指针
    multimap<uint256, CDataStream*> mapOrphanTransactionsByPrev; //key: 父交易的hash value:孤儿交易的指针
    

    3. 总体流程

    下面介绍节点收到单笔交易(loose transaction)并进行处理的整体流程,先贴出流程图


    接下来对流程图中的重要部分做出说明

    ❖交易验证

    处理一笔交易时首先要对交易进行验证,判断能否被放入交易池。验证交易的函数是AcceptTransaction。验证交易的流程会在后面单独展开,这里先提一下。

    bool AcceptTransaction(CTxDB& txdb, bool fCheckInputs=true, bool* pfMissingInputs=NULL);
    
    bool AcceptTransaction(bool fCheckInputs=true, bool* pfMissingInputs=NULL)
    {
        CTxDB txdb("r");
        //调用重载的另一个AcceptTransaction,第一个参数是本地数据库blkindex.dat
        return AcceptTransaction(txdb, fCheckInputs, pfMissingInputs);
    }
    
    ❖加入孤儿交易池

    我们看到,AcceptTransaction函数有一个传出参数是pfMissingInput, 如果交易未能通过验证,且原因是在区块链和交易池中找不到这笔交易的所有父交易,那么传出参数将会被置True,交易会被放入孤儿交易池。这里贴出交易加入孤儿池的代码

    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);
        //孤儿交易的所有父交易,都要和孤儿交易组成键值对,存入multimap
        foreach(const CTxIn& txin, tx.vin)
            mapOrphanTransactionsByPrev.insert(make_pair(txin.prevout.hash, pvMsg));
    }
    
    ❖加入钱包,广播交易

    如果交易通过验证,判断交易的输出中是否有发给本节点的(看交易的锁定脚本的公钥hash),如果有就存入钱包,然后向其他节点转播交易,这涉及到钱包和p2p网络的知识,在本篇文章中不会分析这个子流程。

    ❖排查孤儿交易池

    继续向下看,交易hash值被放入一个名叫vWorkQueue的vector中,这个vector保存通过验证的交易。我们需要遍历这个vector,处理此交易对应的孤儿交易集合。首先获得这个孤儿交易集合,然后遍历集合,对集合的每笔交易进行交易验证(这里验证函数的传出参数pfMissingInputs不生效,因为交易已在孤儿交易池),如果验证通过就加入vWorkQueue。这样形成了递归,凡不再是孤儿交易的交易都会被从孤儿交易池中捞出来,放在vWorkQueue中。我们从流程图下方中的两个循环中可以清楚地看到这个流程,这块的代码也贴出来

    vWorkQueue.push_back(inv.hash);
    
    // Recursively process any orphan transactions that depended on this one
    //递归处理与此笔交易有关的孤儿交易
    for (int i = 0; i < vWorkQueue.size(); i++)
    {
        //此笔交易的hash,对于孤儿交易是父交易
        uint256 hashPrev = vWorkQueue[i];
        //遍历这笔交易对应的孤儿交易
        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());
            //孤儿交易找到一个父交易, 对孤儿交易重新调用AcceptTransaction函数,此时不用填fMissingInput参数,因为孤儿交易已经在孤儿池中
            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);
            }
        }
    }
    
    ❖ 整理孤儿交易池

    最后一步好理解, 孤儿交易池中能捞出来的已经全被捞出来了,对照vWorkQueue整理孤儿交易池的两个map就好了,这里代码就不贴出了。

    4. 验证交易步骤

    在总体流程中为了保持脉络清晰没有展开说验证交易的步骤,实际上验证交易需要很多步骤,先看流程图


    从图中可以看到,验证交易分六个大步骤:

    ❖coinbase检查

    如果收到的交易是coinbase交易,那么验证失败,因为coinbase交易只能出现在区块中,在网络上传播的单笔交易一定是错误的。

    ❖常规检查

    常规检查会查看输入输出是否为空,查看交易额是否为负值,输入来源是否为null。

    ❖交易是否已经存在

    从交易池、区块链账本中查找此笔交易是否已经存在。对于交易池,直接查找mapTransactions就可以了。对于区块链账本,需要查找数据库,对应的数据库文件是blkindex.dat。

    ❖冲突检查

    这是检查交易池的一个关键步骤,如果这笔交易与交易池中的某笔交易所引用的交易是一模一样的,那么可以认定这两笔交易是一样的,只是新旧不同(通过nSequence比较),会用新交易替代旧交易;如果这笔交易与交易池中的某笔交易所引用的交易只是部分相同,这种情况就属于冲突,后收到的交易一定会被节点拒绝。此处的代码初看时有点令人费解,网上有些源码分析的解释是错误的。贴出源码:

    // Check for conflicts with in-memory transactions
    //检查与内存中其他交易的冲突, 只有在内存里找到与当前这笔交易引用的输出是一模一样的交易,才比较新旧
    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,代表第一个没满足条件,就可以直接return false了
            if (i != 0)
                return false;
            ptxOld = mapNextTx[outpoint].ptx;
            //当前交易如果不比旧的新,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;
        }
    }
    
    ❖输入检查

    冲突检查检查的是与交易池中交易的冲突,输入检查检查的则是与区块链中交易的冲突,这个流程比较复杂,会在后面单独展开。

    ❖加入交易池

    如果上面5步的检查都通过了,剩下的工作就是把交易放入交易池了。如果上面的冲突检查中ptxOld不为null, 还要把旧的交易从交易池中移除。很多人觉得交易池很神秘,其实一点也不神秘。贴出将交易添加到交易池的代码,只有几行

    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. 输入检查

    下面展开分析输入检查的流程,这是检查流程中的重要部分,包含了验证签名和双花检查两个核心步骤,先贴出流程图


    下面针对重要的部分展开分析

    ❖查找父交易位置

    我们从图中看到,首先遍历交易的输入序列,然后在本地区块链中查找父交易的位置,如果找不到就在内存中找,在内存中还找不到的话,就会将*pfMissInputs置true, 可以看出只要有一个父交易找不到子交易就被认为是孤儿交易。针对找到的情况,用一个CTxIndex类型的局部变量txIndex保存父交易的位置。如果父交易在区块链中,则位置信息有数据,父交易在交易池中,位置信息无数据。

    ❖父交易coinbase进行检查

    现在我们知道了父交易的位置信息,接下来用一个局部变量txPrev保存父交易,由于coinbase交易必须要等100个确认后矿工的比特币才能被花掉,因此如果父交易是coinbase交易的话,满足100个确认才能验证(比较区块链高度)通过.

    ❖验证签名

    下面进行验证签名检查,验证签名涉及脚本和椭圆曲线加密算法的知识,其原理会在后续的文章展开讨论。这里讲下流程,将子交易的解锁脚本和父交易的锁定脚本连起来,然后调用脚本执行函数,执行过程类似于压栈弹栈操作。执行完脚本后,如果子交易的发起者拥有父交易锁定脚本中公钥hash对应的私钥,那么解锁脚本会解开锁定脚本,函数会返回True, 贴出验证签名的代码

    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);
    }
    
    ❖检查双花

    我们既然拿到了父交易的位置信息,就可以查看这个结构体的vSpent部分,如果txIndex.vSpent[prevout.n]已经被置位了,说明父交易的这个输出已经被花掉了,子交易的输入用了已经被花费掉的输出,这就产生了“双花”(double spend)。因此,验证不会通过。

    ❖标志位置位

    双花检查通过,接下来将txIndex.vSpent[prevout.n]置位,由于我们验证的是单笔交易(loose trsanction), 这笔交易并没有入块,所以这个标志位只要被置位即可,内容没有要求,位置也不会被保存。如果验证的是块中交易,这个标志位要填块的位置,并且这个位置会被写入数据库。

    ❖计算手续费

    遍历完所有父交易后,我们可以累加得到输入的总金额,用这个金额减去输出总金额,就得到了当前交易的手续费,如果手续费大于最小手续费,可以验证通过。

    此处贴出输入检查的源码

    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)
                    {
                        //在内存池(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;
    }
    

    至此,全节点处理交易的流程就理清了,读者最好对照流程图去阅读一遍源码,这样会理解的更加透彻。

    BTech原创,未经许可不得转载

    相关文章

      网友评论

        本文标题:比特币全节点处理交易流程分析

        本文链接:https://www.haomeiwen.com/subject/pmrwbftx.html