比特币全节点处理交易流程分析
我们知道,比特币网络上的交易需要由节点验证并传播,那么当节点收到一笔交易时,到底做了哪些处理?具体的流程又是怎样?使用了什么样的数据结构?网络上的文章大多是泛泛而谈,读者看的云里雾里,难以理出清晰的脉络。本文旨在解决读者的疑惑,通过剖析比特币的源码,勾勒出节点处理交易时的完整流程图。
注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)并进行处理的整体流程,先贴出流程图
![](https://img.haomeiwen.com/i13025278/10f9349e2ad3ece4.png)
接下来对流程图中的重要部分做出说明
❖交易验证
处理一笔交易时首先要对交易进行验证,判断能否被放入交易池。验证交易的函数是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. 验证交易步骤
在总体流程中为了保持脉络清晰没有展开说验证交易的步骤,实际上验证交易需要很多步骤,先看流程图
![](https://img.haomeiwen.com/i13025278/1b91ef76d08725ef.png)
从图中可以看到,验证交易分六个大步骤:
❖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. 输入检查
下面展开分析输入检查的流程,这是检查流程中的重要部分,包含了验证签名和双花检查两个核心步骤,先贴出流程图
![](https://img.haomeiwen.com/i13025278/36cbf0fba7bfe7b3.jpg)
下面针对重要的部分展开分析
❖查找父交易位置
我们从图中看到,首先遍历交易的输入序列,然后在本地区块链中查找父交易的位置,如果找不到就在内存中找,在内存中还找不到的话,就会将*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原创,未经许可不得转载