我們已經(jīng)分析過了全節(jié)點(diǎn)處理單筆交易(loose transaction)的詳細(xì)流程,這篇文章將分析全節(jié)點(diǎn)收到一個區(qū)塊后的處理流程朽合,內(nèi)容包括如何驗證這個區(qū)塊做入,如何更新本地的區(qū)塊鏈賬本缭嫡。
注1:本文的分析以中本聰?shù)拇av0.1.15版本為藍(lán)本喂很,節(jié)點(diǎn)指代為全節(jié)點(diǎn)惜颇。
注2:如果對比特幣區(qū)塊的基本知識還不清楚,請先閱讀MasteringBitcoin這本書的相應(yīng)章節(jié)少辣,此書中文版叫做《精通比特幣》官还。
注3:文中提到的關(guān)鍵步驟會貼出相應(yīng)源碼,非關(guān)鍵部分請參考流程圖自行去看源碼
1. 區(qū)塊鏈存儲方式
我們知道毒坛,比特幣的精髓是去中心化,用白話講就是人人記賬林说,當(dāng)然這里的人人指代的是全節(jié)點(diǎn)煎殷。每個全節(jié)點(diǎn)都會在本地維護(hù)一個區(qū)塊鏈賬本的副本。有人會問腿箩,區(qū)塊鏈的主賬本存儲在哪豪直?其實并沒有主賬本,各節(jié)點(diǎn)通過遵守比特幣的共識機(jī)制珠移,他們的賬本是一致性收斂的弓乙。
那么區(qū)塊鏈在本地是如何存儲的?比特幣使用了Berkeley Db數(shù)據(jù)庫钧惧,這是一個文件型數(shù)據(jù)庫暇韧,數(shù)據(jù)都是以鍵值對的形式存儲在文件中的。區(qū)塊的數(shù)據(jù)存儲在blkxxxx.dat(x代表十進(jìn)制數(shù)字)里浓瞪,從文件的名字可以看出懈玻,需要大量的.dat文件存儲區(qū)塊數(shù)據(jù)。那么在賬本中查找一筆交易會不會很慢乾颁?并不會涂乌,節(jié)點(diǎn)會維護(hù)一個名叫blkindex.dat的文件艺栈,這個文件就像一個地圖一樣,詳細(xì)的記錄了交易在賬本中的位置湾盒。除了交易信息湿右,blkindex.data還保存區(qū)塊索引、主鏈參數(shù)等信息罚勾。
舉例說明blkindex.dat文件存儲數(shù)據(jù)的格式:
- 交易位置 key: pair<'tx', 交易hash> value:位置信息
- 區(qū)塊索引 key: pair<'blockindex', 區(qū)塊hash> value:區(qū)塊索引信息+前后塊hash
- 主鏈參數(shù) key: "hashBestChain" value:最高塊hash
2. Berkeley數(shù)據(jù)庫操作
Berkeley數(shù)據(jù)庫提供了一套C語言的操作接口毅人,中本聰用C++在上面封了一層∮猓基類CDB提供了數(shù)據(jù)庫的增刪改查等基本操作堰塌,類CTxDB、CWallletDB等繼承自CDB分衫,完成一些特定場景的數(shù)據(jù)庫操作场刑。本文討論節(jié)點(diǎn)處理區(qū)塊的流程,只用到了CTxDB處理區(qū)塊數(shù)據(jù), 與之相對應(yīng)的文件是blkindex.dat
下面簡單介紹操作數(shù)據(jù)庫的方式:
- 構(gòu)造數(shù)據(jù)庫操作對象:CTxDB txdb("r+")
- 寫區(qū)塊索引信息: txdb.WriteBlockIndex(blockindexPrev)
- 中止事務(wù): txdb.TxnAbort()
- 提交事務(wù): txdb.Commit()
注:寫數(shù)據(jù)庫事務(wù)并不會立即更新文件內(nèi)容蚪战,提交事務(wù)才會**
3. 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
3.1 關(guān)鍵類
首先是區(qū)塊類牵现,由區(qū)塊頭、交易集合邀桑、merkle樹構(gòu)成
class CBlock
{
public:
// header
int nVersion;
uint256 hashPrevBlock;
uint256 hashMerkleRoot;
unsigned int nTime;
unsigned int nBits;
unsigned int nNonce;
// network and disk
vector<CTransaction> vtx;
// memory only
mutable vector<uint256> vMerkleTree;
/*成員函數(shù)略*/
}
下來是區(qū)塊索引類瞎疼,可以看出這個類主要保存了區(qū)塊的位置信息、與前后塊的聯(lián)系壁畸、區(qū)塊頭的信息贼急。
注意:在內(nèi)存中幾乎都是在對區(qū)塊索引操作,所以此類很重要
class CBlockIndex
{
public:
const uint256* phashBlock; //區(qū)塊hash指針
CBlockIndex* pprev; //前向指針
CBlockIndex* pnext; //后向指針
unsigned int nFile; //第幾個文件block*.dat
unsigned int nBlockPos; //在文件中的位置
int nHeight; //區(qū)塊高度
// block header 區(qū)塊頭信息
int nVersion; //版本
uint256 hashMerkleRoot; //MerkleRoot哈希值
unsigned int nTime; //時間戳
unsigned int nBits; //難度
unsigned int nNonce; //隨機(jī)值
/*成員函數(shù)略*/
}
3.2 關(guān)鍵全局變量
首先是區(qū)塊索引集合捏萍,通過這個集合可以找到所有的區(qū)塊索引信息太抓,它相當(dāng)于厚重的賬本在內(nèi)存中的輕量級映射
map<uint256, CBlockIndex*> mapBlockIndex; //key:區(qū)塊hash value:區(qū)塊索引指針
下面是孤塊區(qū), 我們看到令杈,與孤兒交易池類似走敌,map的value存的是指針,實際上孤塊數(shù)據(jù)都是在堆中保存的逗噩。第二個map是multimap掉丽,因為區(qū)塊鏈會發(fā)生發(fā)叉,一個父區(qū)塊可能有多個子塊异雁。
map<uint256, CBlock*> mapOrphanBlocks; // key:hash value: 區(qū)塊指針
multimap<uint256, CBlock*> mapOrphanBlocksByPrev; //key: 父區(qū)塊 hash value:區(qū)塊指針
4. 總體流程
下面分析節(jié)點(diǎn)處理區(qū)塊的整體流程捶障,先貼出整體流程圖
接下來對重要的步驟進(jìn)行說明
?常規(guī)檢查
常規(guī)檢查是對塊本身的合法性進(jìn)行檢查,并不涉及與其他塊的聯(lián)系纲刀,比較簡單残邀。主要包括以下幾點(diǎn)
- 區(qū)塊大小限制檢查 (包括上限下限)
- 時間戳檢查 (允許時間戳+本地時間2小時內(nèi)的區(qū)塊)
- 塊中第一筆交易必須是coinbase交易,其余必須不是
- 塊中每筆交易的常規(guī)檢查 (處理交易流程提到)
- 工作量檢查 (區(qū)塊hash小于難度設(shè)定)
- merkle根檢查 (構(gòu)建merlele樹,驗證merkele根計算是否正確)
?查找父區(qū)塊
常規(guī)檢查通過后芥挣,需要在mapBlockIndex中查找父區(qū)塊驱闷,如果找不到,指針要被放到孤塊區(qū)空免,然后節(jié)點(diǎn)會向塊的來源發(fā)getblock命令空另,試圖拿到這個塊的父區(qū)塊;如果能找到父區(qū)塊才會調(diào)用AcceptBlock函數(shù)對區(qū)塊進(jìn)一步檢查蹋砚。此處貼出這個步驟的代碼
// If don't already have its previous block, shunt it off to holding area until we get it
//在mapBlcokIndex中找不到前向區(qū)塊扼菠,分流到孤塊區(qū)
if (!mapBlockIndex.count(pblock->hashPrevBlock))
{
printf("ProcessBlock: ORPHAN BLOCK, prev=%s\n", pblock->hashPrevBlock.ToString().substr(0,14).c_str());
mapOrphanBlocks.insert(make_pair(hash, pblock));
mapOrphanBlocksByPrev.insert(make_pair(pblock->hashPrevBlock, pblock));
// Ask this guy to fill in what we're missing
//如果這個節(jié)點(diǎn)給我們發(fā)的是孤塊,那么向這個節(jié)點(diǎn)發(fā)getblocks坝咐,試圖獲得丟掉的塊
if (pfrom)
pfrom->PushMessage("getblocks", CBlockLocator(pindexBest), GetOrphanRoot(pblock));
//是孤塊直接返回true
return true;
}
?AcceptBlock
與驗證交易的函數(shù)AcceptTransaction相似循榆,AcceptBlock是個復(fù)雜的流程,會在后面單獨(dú)展開墨坚。繼續(xù)向下分析主流程
?整理孤塊區(qū)
如果區(qū)塊通過AcceptBlock驗證秧饮,接下來要進(jìn)行孤塊區(qū)的整理工作。與排查整理孤兒交易池的操作類似(還要簡單一些)泽篮,首先將區(qū)塊hash放入集合vWorkQueue中盗尸,然后遍歷這個集合,通過查mapOrphanBlocksByPrev獲取以此塊為父塊的孤塊集合帽撑,接著遍歷孤塊集合泼各,對每個塊都調(diào)用AcceptBlock函數(shù),將驗證通過的塊的hash值繼續(xù)放入vWorkQueue, 形成遞歸亏拉。
無論是否通過AcceptBlock驗證扣蜻,子塊都己經(jīng)不是孤塊,需要從mapOrphanBlocks中刪除及塘。
遍歷完孤塊集合后弱贼,要在mapOrphanBlocksByPrev中把以父塊為key的條目也刪掉。流程圖中下方的兩個循環(huán)展示了這個流程磷蛹,貼出此處代碼
// Recursively process any orphan blocks that depended on this one
vector<uint256> vWorkQueue;
vWorkQueue.push_back(hash);
for (int i = 0; i < vWorkQueue.size(); i++)
{
uint256 hashPrev = vWorkQueue[i];
for (multimap<uint256, CBlock*>::iterator mi = mapOrphanBlocksByPrev.lower_bound(hashPrev);
mi != mapOrphanBlocksByPrev.upper_bound(hashPrev);
++mi)
{
CBlock* pblockOrphan = (*mi).second;
if (pblockOrphan->AcceptBlock())
vWorkQueue.push_back(pblockOrphan->GetHash());
mapOrphanBlocks.erase(pblockOrphan->GetHash());
delete pblockOrphan;
}
mapOrphanBlocksByPrev.erase(hashPrev);
}
5. AcceptBlock流程
下面分析AcceptBlock函數(shù),先貼出流程圖
接著對重要步驟作出說明
?時間戳檢查
我們看到AcceptBlock流程中除了重復(fù)塊檢查外其余的檢查都和區(qū)塊鏈的歷史區(qū)塊產(chǎn)生了聯(lián)系溪烤,此處的時間戳檢查會將此塊的時間戳與過去11個塊的時間戳中位數(shù)進(jìn)行對比味咳。舉個通俗的例子作類比,如果過去11個塊的時間戳中位數(shù)是12點(diǎn)檬嘀,新塊的時間戳是11點(diǎn)59槽驶,那么新塊會被拒絕,如果新塊的時間戳是12點(diǎn)01鸳兽,新塊會被接受掂铐,貼出代碼
// Check timestamp against prev 比較時間戳與前11個區(qū)塊中位數(shù)
if (nTime <= pindexPrev->GetMedianTimePast())
return error("AcceptBlock() : block's timestamp is too early");
?難度檢查
難度檢查會以過去2016個塊的難度為基準(zhǔn)推算下一個塊的難度,要檢查新塊的難度是否等于推算的難度。
// Check proof of work 從前一個區(qū)塊推全陨,檢查難度對不對
if (nBits != GetNextWorkRequired(pindexPrev))
return error("AcceptBlock() : incorrect proof of work");
?區(qū)塊寫入硬盤
接下來檢查硬盤空間是否充足爆班,然后把區(qū)塊寫入硬盤,此處有兩個傳出參數(shù)辱姨,記錄區(qū)塊的位置柿菩。
注:區(qū)塊數(shù)據(jù)是直接被附加到最新的blkxxxx.dat文件中的,不走數(shù)據(jù)庫事務(wù)
// Write block to history file 檢查硬盤空間是否充足
if (!CheckDiskSpace(::GetSerializeSize(*this, SER_DISK)))
return error("AcceptBlock() : out of disk space");
unsigned int nFile; //第幾個文件 blk000x.dat
unsigned int nBlockPos; //文件中的位置
//寫入硬盤雨涛, nFile和nBlockPos是傳出參數(shù)
if (!WriteToDisk(!fClient, nFile, nBlockPos))
return error("AcceptBlock() : WriteToDisk failed");
?AddToBlockIndex
接下來又是關(guān)鍵的一步枢舶,區(qū)塊已經(jīng)寫入硬盤,位置信息作為參數(shù)傳入AddToBlockIndex函數(shù)替久。從函數(shù)名就可以看出凉泄,要對mapBlockIndex進(jìn)行操作了,實際上沒那么簡單蚯根,在這個函數(shù)里會對區(qū)塊繼續(xù)做一系列的檢查后众,先不展開分析,繼續(xù)向下分析流程圖稼锅。
?轉(zhuǎn)播區(qū)塊
如果AddToBlockIndex也通過了吼具,會判斷這個區(qū)塊是不是在主鏈上,如果在就會向其他節(jié)點(diǎn)轉(zhuǎn)播這個區(qū)塊
if (hashBestChain == hash)
RelayInventory(CInv(MSG_BLOCK, hash));
注意:區(qū)塊鏈會發(fā)生分叉矩距,產(chǎn)生主鏈和備用鏈拗盒,共識機(jī)制會認(rèn)可難度最大的鏈,通常就是長度最長的鏈锥债。
6. AddToBlockIndex流程
下面分析AddToBlockIndex函陡蝇,先貼出流程圖
接下來解釋關(guān)鍵步驟
?更新mapBlockIndex
上一步獲取了新區(qū)塊的位置,作為參數(shù)傳了進(jìn)來哮肚,接下來構(gòu)造出新的區(qū)塊索引對象登夫,將鍵值對插入到mapBlockIndex中,然后找到父區(qū)塊允趟,新區(qū)塊索引前向指針指向父區(qū)塊索引恼策,然后高度+1。用文字描述比較拗口潮剪,貼出代碼
// 根據(jù)這個塊生成新的區(qū)塊索引
CBlockIndex* pindexNew = new CBlockIndex(nFile, nBlockPos, *this);
if (!pindexNew)
return error("AddToBlockIndex() : new CBlockIndex failed");
//新的區(qū)塊索引插入mapBlockIndex中
map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
pindexNew->phashBlock = &((*mi).first);
//mapBlockIndex中查找父區(qū)塊索引
map<uint256, CBlockIndex*>::iterator miPrev = mapBlockIndex.find(hashPrevBlock);
//新區(qū)塊索引前向指針指向父塊涣楷,高度 = 父區(qū)塊高度 + 1
if (miPrev != mapBlockIndex.end())
{
pindexNew->pprev = (*miPrev).second;
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
}
?新區(qū)塊索引信息寫入數(shù)據(jù)庫
注意,此時只是寫數(shù)據(jù)庫事務(wù)抗碰,還沒有commit狮斗,數(shù)據(jù)還未真正寫入硬盤。區(qū)塊索引在blkindex.dat文件中的保存形式在文中已經(jīng)提到過弧蝇。
//寫入blkindex.data
CTxDB txdb;
txdb.TxnBegin();
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
?判斷區(qū)塊高度是否創(chuàng)新高
如果區(qū)塊高度沒創(chuàng)新高碳褒,說明此塊在一條短鏈上折砸,暫時不需要對這個區(qū)塊做其他檢查了,待到這條鏈“逆襲”成為主鏈時再對這個塊做其余的檢查也不遲沙峻,數(shù)據(jù)庫直接commit睦授,流程就結(jié)束了。
如果區(qū)塊高度創(chuàng)了新高专酗,說明此塊一定在主鏈上睹逃,至于此塊在原主鏈上還是在新主鏈上還需要判斷父區(qū)塊在不在主鏈上。
?檢查輸入ConnectBlock
如果父區(qū)塊在主鏈上祷肯,接下來要對新塊執(zhí)行ConnectBlock函數(shù)沉填,檢查輸入。ConnectBlock函數(shù)相對來講流程較少佑笋,直接貼出流程圖
我們看到ConnectBlock函數(shù)遍歷了塊中的交易翼闹,對每筆交易執(zhí)行了ConnectInputs函數(shù),ConectInputs函數(shù)的流程在分析交易處理的文章中已經(jīng)詳細(xì)的分析過了蒋纬。如果ConnectInputs驗證通過猎荠,檢查coinbase交易的金額是否正確,我們知道比特幣的產(chǎn)量是遞減的蜀备,大概每四年減半关摇。接著寫數(shù)據(jù)庫事務(wù),更新父區(qū)塊的索引信息(就是更新后向區(qū)塊的hash值)碾阁,然后將塊中交易同本節(jié)點(diǎn)有關(guān)(發(fā)給本節(jié)點(diǎn)的)的加入錢包输虱,貼出ConnectBlock源碼
bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex)
{
//// issue here: it doesn't know the version
//交易的位置
unsigned int nTxPos = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK) - 1 + GetSizeOfCompactSize(vtx.size());
map<uint256, CTxIndex> mapUnused;
int64 nFees = 0;
foreach(CTransaction& tx, vtx)
{
CDiskTxPos posThisTx(pindex->nFile, pindex->nBlockPos, nTxPos);
nTxPos += ::GetSerializeSize(tx, SER_DISK);
//對塊中每一筆交易進(jìn)行ConnectInputs驗證
if (!tx.ConnectInputs(txdb, mapUnused, posThisTx, pindex->nHeight, nFees, true, false))
return false;
}
if (vtx[0].GetValueOut() > GetBlockValue(nFees))
return false;
// Update block index on disk without changing it in memory.
// The memory index structure will be changed after the db commits.
//寫數(shù)據(jù)庫事務(wù),更新父區(qū)塊索引信息
if (pindex->pprev)
{
CDiskBlockIndex blockindexPrev(pindex->pprev);
blockindexPrev.hashNext = pindex->GetBlockHash();
txdb.WriteBlockIndex(blockindexPrev);
}
// Watch for transactions paying to me
//如果塊中中交易是給自己的脂凶,加入錢包
foreach(CTransaction& tx, vtx)
AddToWalletIfMine(tx, this);
return true;
}
?ConnectBlock返回True
如果ConnectBlock函數(shù)返回True宪睹,對區(qū)塊的所有檢查都結(jié)束了,僅剩下一些收尾工作蚕钦。流程圖已經(jīng)標(biāo)注的很清晰了亭病。
?ConnectBlock返回False
如果ConnectBlock函數(shù)返回False, 區(qū)塊的驗證失敗,需要從硬盤將區(qū)塊數(shù)據(jù)刪除嘶居,從mapBlockIndex中刪除這個區(qū)塊索引罪帖。
if (!ConnectBlock(txdb, pindexNew) || !txdb.WriteHashBestChain(hash))
{
txdb.TxnAbort();
pindexNew->EraseBlockFromDisk();
mapBlockIndex.erase(pindexNew->GetBlockHash());
delete pindexNew;
return error("AddToBlockIndex() : ConnectBlock failed");
}
7. 重整主鏈(Reorganize)
在AddToBlockIndex的流程圖中,我們只分析了父區(qū)塊在主鏈上這一種情況邮屁。如果父區(qū)塊不在主鏈上整袁,此時區(qū)塊高度還創(chuàng)了新高,那么只有一種情況樱报,就是備用鏈實現(xiàn)了“逆襲”,成為了主鏈泞当,這是需要調(diào)用Reorganize函數(shù)迹蛤,重整主鏈。這塊內(nèi)容可以結(jié)合主鏈維護(hù)算法那篇文章一起看。貼出Reorganize函數(shù)流程圖
下面對重要步驟作出解釋
?回退區(qū)塊指針至分叉點(diǎn)
就是將新鏈和舊鏈指針回退盗飒,直到找到分叉的位置嚷量。
// Find the fork
CBlockIndex* pfork = pindexBest;
CBlockIndex* plonger = pindexNew;
while (pfork != plonger)
{
//pfork從pindexBest回退一步,找到分叉的位置
if (!(pfork = pfork->pprev))
return error("Reorganize() : pfork->pprev is null");
//plonger從另一個叉也回退到分叉的位置
while (plonger->nHeight > pfork->nHeight)
if (!(plonger = plonger->pprev))
return error("Reorganize() : plonger->pprev is null");
}
?三個集合vConnect逆趣、vDisconnect蝶溶、vResurrect
- vConnect: 從分叉點(diǎn)開始算,存儲新杈上的區(qū)塊索引
- vDisconnect: 從分叉點(diǎn)開始算宣渗,存儲舊杈上的區(qū)塊索引
- vResurrent: 存儲舊杈上的區(qū)塊包含的除coinbase交易外的所有交易
// List of what to disconnect
//把原Bestchain從tip到分叉點(diǎn)的節(jié)點(diǎn)放入vDisconnect
vector<CBlockIndex*> vDisconnect;
for (CBlockIndex* pindex = pindexBest; pindex != pfork; pindex = pindex->pprev)
vDisconnect.push_back(pindex);
// List of what to connect
//從分叉點(diǎn)到新tip的節(jié)點(diǎn)放入vConnect
vector<CBlockIndex*> vConnect;
for (CBlockIndex* pindex = pindexNew; pindex != pfork; pindex = pindex->pprev)
vConnect.push_back(pindex);
reverse(vConnect.begin(), vConnect.end());
// Disconnect shorter branch
//原來的BestChain中抖所,需要重新整理的那幾個塊的交易集合
vector<CTransaction> vResurrect;
?遍歷處理集合vDisconnect
這是一條即將失效的鏈,有很多工作要做痕囱。這里講下主要流程田轧,遍歷這個集合,從硬盤中加載區(qū)塊數(shù)據(jù)鞍恢,然后對塊中所有交易執(zhí)行DisconnectInputs函數(shù)傻粘,這個函數(shù)會寫數(shù)據(jù)庫事務(wù),將失效塊中交易的父交易的輸出花費(fèi)標(biāo)志位置null帮掉,然后保存弦悉。如果Reorganize函數(shù)執(zhí)行失敗了,數(shù)據(jù)庫不會commit的蟆炊,所以這里不必?fù)?dān)心意外情況稽莉。接著把失效塊中的交易除coinbase交易外放入vResurrent中
foreach(CBlockIndex* pindex, vDisconnect)
{
CBlock block;
if (!block.ReadFromDisk(pindex->nFile, pindex->nBlockPos, true))
return error("Reorganize() : ReadFromDisk for disconnect failed");
//此塊中的交易數(shù)據(jù)需要斷掉與之前Input數(shù)據(jù)的關(guān)聯(lián)
if (!block.DisconnectBlock(txdb, pindex))
return error("Reorganize() : DisconnectBlock failed");
// Queue memory transactions to resurrect
//這些塊中的交易,除了coinbase交易都放入vResurrent中
foreach(const CTransaction& tx, block.vtx)
if (!tx.IsCoinBase())
vResurrect.push_back(tx);
}
?遍歷處理集合vConnect
這是一條即將生效的鏈盅称,也有很多工作要做肩祥。我們記得上文中提到過“待到這條鏈逆襲時再檢查不遲”,現(xiàn)在就是檢查的時候了缩膝。從硬盤中加載區(qū)塊數(shù)據(jù)混狠,對每個塊調(diào)用ConnectBlock函數(shù),如果有某個塊驗證失敗疾层,以此塊為起點(diǎn)的整條鏈都是錯誤的将饺,要刪除鏈上這部分的數(shù)據(jù)。如果全部通過痛黎,將塊中的交易放到集合vDelete中, 待從交易池刪除予弧。
// Connect longer branch
vector<CTransaction> vDelete;
for (int i = 0; i < vConnect.size(); i++)
{
CBlockIndex* pindex = vConnect[i];
CBlock block;
if (!block.ReadFromDisk(pindex->nFile, pindex->nBlockPos, true))
return error("Reorganize() : ReadFromDisk for connect failed");
if (!block.ConnectBlock(txdb, pindex))
{
// Invalid block, delete the rest of this branch
txdb.TxnAbort();
for (int j = i; j < vConnect.size(); j++)
{
CBlockIndex* pindex = vConnect[j];
pindex->EraseBlockFromDisk();
txdb.EraseBlockIndex(pindex->GetBlockHash());
mapBlockIndex.erase(pindex->GetBlockHash());
delete pindex;
}
return error("Reorganize() : ConnectBlock failed");
}
// Queue memory transactions to delete
foreach(const CTransaction& tx, block.vtx)
vDelete.push_back(tx);
}
?重整主鏈?zhǔn)瘴补ぷ?/h6>
流程圖中收尾工作的流程很清晰,需要解釋的是要對vResurrect集合中的交易重新調(diào)用AcceptTransaction函數(shù)湖饱,如果通過就放入交易池掖蛤。我們可以看出,舊鏈?zhǔn)Ь幔驹谫~本中的交易被剔除出來蚓庭,需要重新驗證才能加入交易池致讥,如果交易池中有惡意的雙花交易與此筆想要重新加入交易池的交易沖突,那此筆交易就會被雙花交易擠掉器赞。這就是“雙花”攻擊成功的一種現(xiàn)象垢袱,本來已經(jīng)有一個確認(rèn)的交易沒能繼續(xù)在區(qū)塊鏈賬本中“生存”。當(dāng)然如果原主鏈發(fā)力奪回最長鏈的話港柜,劇本還將改寫请契,這里只是提示一個確認(rèn)并不安全,越多確認(rèn)越安全夏醉。
至此爽锥,全節(jié)點(diǎn)處理區(qū)塊的流程也分析完了,這個流程還是比較復(fù)雜的授舟,涉及到很多細(xì)節(jié)救恨,梳理的過程中會有很多疑問。帶著疑問找到流程圖的相應(yīng)部分释树,再找到相應(yīng)的代碼閱讀肠槽,對解決疑問有很大幫助,可以避免迷失在代碼的海洋中奢啥。
BTech原創(chuàng)秸仙,未經(jīng)許可不得轉(zhuǎn)載