区块链比特币学习 - 4 - 交易池

Posted 宣之于口

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了区块链比特币学习 - 4 - 交易池相关的知识,希望对你有一定的参考价值。

比特币学习 - 4 - 交易池

参考博客:here and here and here

在上一篇文章,我们看到了一笔交易的创建,产生的交易随后将被发送到比特币网络临近的节点,从而使得该交易能够在整个比特币网络中传播。

一、基本概念

内存池也称作交易池,用来暂存尚未被加入到区块的交易记录。同时节点还要将交易广播到网络中,其他节点对交易进行验证,无误以后也加入到自己的交易池里。

当交易在网络上广播之后,就会被加进交易池, 但不是所有交易都会被加入交易池,例如:

  • 不符合最低收费标准的交易
  • “双花”交易
  • 非标准交易

交易池中排序规则(优先->次):交易哈希,交易费(包括所有子孙交易),在交易池中的时间,祖先交易费

另外,为了保证交易费的正确性,当新交易被加进交易池中时,我们必须更新该交易的所有祖先交易信息,而这个操作可能会导致处理速度变慢,所以必须对更需祖先的大小和费用进行限制。

二、数据结构

代码目录:src/txmempool.h

1. 交易池中基本单元

class CTxMemPoolEntry

private:
    CTransactionRef tx;		   // 交易引用
    CAmount nFee;              // 交易费用
    size_t nTxWeight;          //!< ... and avoid recomputing tx weight (also used for GetTxSize())
    size_t nUsageSize;         // 大小
    int64_t nTime;             // 时间戳
    unsigned int entryHeight;  // 区块高度
    bool spendsCoinbase;       // 前一个交易是否是coinbase[创币交易]
    int64_t sigOpCost;         //!< Total sigop cost
    int64_t feeDelta;          // 交易优先级的一个标量
    LockPoints lockPoints;     // 交易最后的所在区块高度和打包的时间

    // 交易池中关于该交易的子孙交易;如果移除交易,我们必须同时移除它所有的子孙交易
    uint64_t nCountWithDescendants;  // 子孙交易数量
    uint64_t nSizeWithDescendants;   // 大小
    CAmount nModFeesWithDescendants; // 总费用,包括当前交易

    // 祖先交易信息
    uint64_t nCountWithAncestors;
    uint64_t nSizeWithAncestors;
    CAmount nModFeesWithAncestors;
    int64_t nSigOpCostWithAncestors;
    ...

2. 内存交易池

class CTxMemPool

private:
    uint32_t nCheckFrequency GUARDED_BY(cs); // //表示在2^32时间内检查的次数
    unsigned int nTransactionsUpdated; //!< Used by getblocktemplate to trigger CreateNewBlock() invocation
    CBlockPolicyEstimator* minerPolicyEstimator;

    uint64_t totalTxSize;      // 所有交易池中交易的虚拟大小,不包括见证数据
    uint64_t cachedInnerUsage; // map中元素使用的动态内存大小之和

    mutable int64_t lastRollingFeeUpdate;
    mutable bool blockSinceLastRollingFeeBump;
    mutable double rollingMinimumFeeRate; // 进入pool需要的最小费用
	...
    // 根据之前提到的规则进行排序
    ...
public:
    indirectmap<COutPoint, const CTransaction*> mapNextTx GUARDED_BY(cs);
    std::map<uint256, CAmount> mapDeltas;

    // 创建一个新的交易池
    explicit CTxMemPool(CBlockPolicyEstimator* estimator = nullptr);

    // 安全性检查(不包括双花)
    void check(const CCoinsViewCache *pcoins) const;
    void setSanityCheck(double dFrequency = 1.0)  LOCK(cs); nCheckFrequency = static_cast<uint32_t>(dFrequency * 4294967295.0); 
	...
;

// 注意:mapTx
// 内存池中通过一个boost::multi_index类型的变量mapTx来排序所有交易

三、交易池

验证交易后,比特币节点会将这些交易添加到自己的内存池中。

static UniValue sendrawtransaction(const JSONRPCRequest& request)

    ...
    // 判断交易的输出是否有被花费了
    bool fHaveChain = false;
    for (size_t o = 0; !fHaveChain && o < tx->vout.size(); o++) 
        const Coin& existingCoin = view.AccessCoin(COutPoint(hashTx, o));
        fHaveChain = !existingCoin.IsSpent();
    
    // 判断交易是否已经存在交易池中了
    bool fHaveMempool = mempool.exists(hashTx);
    // 如果交易不在交易池中,并且交易的所有输出都没有被花费,则尝试将交易加入交易池中
    if (!fHaveMempool && !fHaveChain) 
       	...
        // 验证交易有效性,验证通过则将交易加入交易池中
        if (!AcceptToMemoryPool( ... )  ...  
        else 
            CallFunctionInValidationInterfaceQueue([&promise] 
                promise.set_value();
            );
        
	... 
	// 发送一个INV消息,将交易广播到网络中
    CInv inv(MSG_TX, hashTx);
    g_connman->ForEachNode([&inv](CNode* pnode)
    
        pnode->PushInventory(inv);
    );


其中我们来详细看一下AcceptToMemoryPool:以下是关键函数调用

主要部分在于AcceptToMemoryPoolWorker:验证交易以及存储交易

1. 验证交易

在交易传递到临近的节点前,每一个收到交易的比特币节点将会首先验证该交易,这将确保只有有效的交易才会在网络中传播,而无效的交易将会在第一个节点处被废弃。

以下举例几个条件:

  • 交易的语法和数据结构必须正确
  • 输入与输出列表都不能为空
  • 对于每一个输入,引用的输出是必须存在的,并且没有被花费

2. 存储交易

// 截取了存储交易的部分
...
CTxMemPoolEntry entry(ptx, nFees, nAcceptTime, chainActive.Height(),fSpendsCoinbase, nSigOpsCost, lp);
...                 
// Store transaction in memory
pool.addUnchecked(hash, entry, setAncestors, validForFeeEstimation);

继续深入看一下addUnchecked方法:把交易加入内存交易池且不做检查,用于AcceptToMemoryPool(),因为在上面已经完成了各种检查

void CTxMemPool::addUnchecked(const uint256& hash, const CTxMemPoolEntry &entry, setEntries &setAncestors, bool validFeeEstimate)

    NotifyEntryAdded(entry.GetSharedTx());
    indexed_transaction_set::iterator newit = mapTx.insert(entry).first;
    mapLinks.insert(make_pair(newit, TxLinks()));

    // 更新交易优先级
    std::map<uint256, CAmount>::const_iterator pos = mapDeltas.find(hash);
    if (pos != mapDeltas.end()) 
        const CAmount &delta = pos->second;
        if (delta) 
            mapTx.modify(newit, update_fee_delta(delta));
        
    

    // 更新map中元素使用的动态内存大小之和
    cachedInnerUsage += entry.DynamicMemoryUsage();

    const CTransaction& tx = newit->GetTx();
    std::set<uint256> setParentTransactions;
    for (unsigned int i = 0; i < tx.vin.size(); i++) 
        mapNextTx.insert(std::make_pair(&tx.vin[i].prevout, &tx));
        setParentTransactions.insert(tx.vin[i].prevout.hash);
    
    
    // 更新祖先交易
    for (const uint256 &phash : setParentTransactions) 
        txiter pit = mapTx.find(phash);
        if (pit != mapTx.end()) 
            UpdateParent(newit, pit, true);
        
    
    UpdateAncestorsOf(true, newit, setAncestors);
    UpdateEntryForAncestors(newit, setAncestors);

    nTransactionsUpdated++;
    totalTxSize += entry.GetTxSize();
    if (minerPolicyEstimator) minerPolicyEstimator->processTransaction(entry, validFeeEstimate);

    vTxHashes.emplace_back(tx.GetWitnessHash(), newit);
    newit->vTxHashesIdx = vTxHashes.size() - 1;

四、交易广播

节点A通过INV消息将交易hash广播到网络中,对等节点B收集自己没有的交易哈希,达到一定数目(1000)向节点A请求交易数据,然后A将对应交易数据发送给B

接下来根据这个图示流程来看一下代码实现:

代码目录:src/net_processing.cpp

// 截取INV这一段
else if (strCommand == NetMsgType::INV) 
    ...
	// 处理每一笔INV消息
    for (CInv &inv : vInv)
    
        ...
		// 判断交易是否已经存在于区块链上
        bool fAlreadyHave = AlreadyHave(inv);
        ...
        if (inv.type == MSG_TX)  inv.type |= nFetchFlags;
		// 如果是一个区块
        if (inv.type == MSG_BLOCK)  ... 
        // 如果是一笔交易
        else
        
            pfrom->AddInventoryKnown(inv);
            if (fBlocksOnly) 
                LogPrint(BCLog::NET, "transaction (%s) inv sent in violation of protocol peer=%d\\n", inv.hash.ToString(), pfrom->GetId());
             else if (!fAlreadyHave && !fImporting && !fReindex && !IsInitialBlockDownload()) 
                // 如果交易不存在,则将该交易加入到请求集合中
                pfrom->AskFor(inv);
            
        
    

看一下AskFor:将交易加入到mapAskFor

void CNode::AskFor(const CInv& inv)

    ...
    // 将mapAskFor作为优先级队列,key对应的是请求最早被发送的时间
    int64_t nRequestTime;
    limitedmap<uint256, int64_t>::const_iterator it = mapAlreadyAskedFor.find(inv.hash);
    if (it != mapAlreadyAskedFor.end())
        nRequestTime = it->second;
    else
        nRequestTime = 0;
    LogPrint(BCLog::NET, "askfor %s  %d (%s) peer=%d\\n", inv.ToString(), nRequestTime, FormatISO8601Time(nRequestTime/1000000), id);

    // 确保不要时间索引让事情在同一个顺序
    int64_t nNow = GetTimeMicros() - 1000000; //单位到微秒
    static int64_t nLastTime;
    //如果调用很快的话,可以保证对应的++nlastTime使得对应的时间不一样
    ++nLastTime;
    nNow = std::max(nNow, nLastTime);  
    nLastTime = nNow;
	...
    mapAskFor.insert(std::make_pair(nRequestTime, inv));

接下来处理mapAskFor中的数据:SendMessage方法,遍历mapAskFor集合,生成GETDATA消息并发送,从而获取到对应的交易(或者区块)数据

// 截取片段
// Message: getdata (non-blocks)
while (!pto->mapAskFor.empty() && (*pto->mapAskFor.begin()).first <= nNow)

	const CInv& inv = (*pto->mapAskFor.begin()).second;
	if (!AlreadyHave(inv))
	
        // 交易(或者区块)数据不存在,插入vGetData集合,当集合中数据达到1000+时,发送GETDATA消息批量获取数据
		LogPrint(BCLog::NET, "Requesting %s peer=%d\\n", inv.ToString(), pto->GetId());
		vGetData.push_back(inv);
		if (vGetData.size() >= 1000)
		
			connman->PushMessage(pto, msgMaker.Make(NetMsgType::GETDATA, vGetData));
			vGetData.clear();
		
	 else 
		// 已经存在了,从集合里删除
		pto->setAskFor.erase(inv.hash);
	
    // 从队列中删除顶端元素
	pto->mapAskFor.erase(pto->mapAskFor.begin());

// 如果集合不为空
if (!vGetData.empty())
connman->PushMessage(pto, msgMaker.Make(NetMsgType::GETDATA, vGetData));

当节点A收到GETDATA信息,接下来的处理:查看自身是否有该交易数据,有则一条一条发送给请求方

void static ProcessGetData(CNode* pfrom, const CChainParams& chainparams, CConnman* connman, const std::atomic<bool>& interruptMsgProc)

    ...
    // 遍历集合
    while (it != pfrom->vRecvGetData.end() && (it->type == MSG_TX || it->type == MSG_WITNESS_TX)) 
       ...
       // 检查mapRelay或者内存交易池中是否存在交易,如果存在发送TX消息,将交易数据发送给请求方
       bool push = false;
        auto mi = mapRelay.find(inv.hash);
        int nSendFlags = (inv.type == MSG_TX ? SERIALIZE_TRANSACTION_NO_WITNESS : 0);
        if (mi != mapRelay.end()) 
            connman->PushMessage(pfrom, msgMaker.Make(nSendFlags, NetMsgType::TX, *mi->second));
            push = true;
         else if (pfrom->timeLastMempoolReq) 
            auto txinfo = mempool.info(inv.hash);
            // To protect privacy, do not answer getdata using the mempool when
            // that TX couldn't have been INVed in reply to a MEMPOOL request.
            if (txinfo.tx && txinfo.nTime <= pfrom->timeLastMempoolReq) 
                connman->PushMessage(pfrom, msgMaker.Make(nSendFlags, NetMsgType::TX, *txinfo.tx));
                push = true;
            
        
        if (!push) 
            vNotFound.push_back(inv);
        
    
	...
	// 删除元素
	pfrom->vRecvGetData.erase(pfrom->vRecvGetData.begin(), it);
	// 没有数据
	if (!vNotFound.empty()) 
    	connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::NOTFOUND, vNotFound));
	

最后,节点B接收到TX消息后,经过处理后加入交易池。

以上是关于区块链比特币学习 - 4 - 交易池的主要内容,如果未能解决你的问题,请参考以下文章

区块链比特币学习 - 5 -创币交易

从零开始学习区块链

区块链学习交易

比特币-架构原理

区块链学习笔记4——BTC实现

区块链精通比特币学习笔记