区块链比特币学习 - 4 - 交易池
Posted 宣之于口
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了区块链比特币学习 - 4 - 交易池相关的知识,希望对你有一定的参考价值。
比特币学习 - 4 - 交易池
在上一篇文章,我们看到了一笔交易的创建,产生的交易随后将被发送到比特币网络临近的节点,从而使得该交易能够在整个比特币网络中传播。
一、基本概念
内存池也称作交易池,用来暂存尚未被加入到区块的交易记录。同时节点还要将交易广播到网络中,其他节点对交易进行验证,无误以后也加入到自己的交易池里。
当交易在网络上广播之后,就会被加进交易池, 但不是所有交易都会被加入交易池,例如:
- 不符合最低收费标准的交易
- “双花”交易
- 非标准交易
交易池中排序规则(优先->次):交易哈希,交易费(包括所有子孙交易),在交易池中的时间,祖先交易费
另外,为了保证交易费的正确性,当新交易被加进交易池中时,我们必须更新该交易的所有祖先交易信息,而这个操作可能会导致处理速度变慢,所以必须对更需祖先的大小和费用进行限制。
二、数据结构
代码目录: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 - 交易池的主要内容,如果未能解决你的问题,请参考以下文章