比特幣原始碼分析:VersionBits模組解析
BIP9允許部署多個向後相容的軟分叉,通過曠工在一個目標週期內投票,如果達到啟用閾值nRuleChangeActivationThreshold
,就能成功的啟用該升級。在實現方面,通過重定義區塊頭資訊中的version欄位,將version欄位解釋為bit vector,每一個bit可以用來跟蹤一個獨立的部署,在滿足啟用條件之後,該部署將會生效,同時該bit可以被其他部署使用。目前通過BIP9成功進行軟分叉有BIP68, 112, 113
, 於2016-07-04 ,高度:419328成功啟用.
BIP9部署設定
每一個進行部署的BIP9,都必須設定bit位、開始時間、過期時間。
struct BIP9Deployment {
int bit;
int64_t nStartTime;
int64_t nTimeout;
};
// namespace:Consensus
struct Params {
...
uint32_t nRuleChangeActivationThreshold;
uint32_t nMinerConfirmationWindow;
BIP9Deployment vDeployments[MAX_VERSION_BITS_DEPLOYMENTS]; // BIP9
uint256 powLimit;
bool fPowAllowMinDifficultyBlocks;
bool fPowNoRetargeting;
int64_t nPowTargetSpacing;
int64_t nPowTargetTimespan;
...
};
bit通過1 << bit
方式轉換成一個uint32_t的整數,在檢驗一個BIP9部署是否成功啟用的時候使用了Condition(...)函式,來驗證一個區塊是否贊成該部署。
bool Condition(const CBlockIndex *pindex, const Consensus::Params ¶ms) const {
return ((
(pindex->nVersion & VERSIONBITS_TOP_MASK) ==VERSIONBITS_TOP_BITS) &&
(pindex->nVersion & Mask(params)) != 0);
}
uint32_t Mask(const Consensus::Params ¶ms) const {
return ((uint32_t)1) << params.vDeployments[id].bit;
}
邏輯分析
- 首先驗證該version是有效的version設定(001)
- 驗證塊的版本號中是否設定了指定的bit位
- Mask()函式通過將1左移BIP9部署中設定的bit,生成一個該區塊代表的version
開始時間和過期時間主要為了在檢查BIP9部署狀態時,提供狀態判斷的依據和臨界值。比如如果區塊的中位數時間超過了過期時間nTimeTimeout
,則判斷該BIP9部署已經失敗(後面會詳細拆解)。
if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) {
stateNext = THRESHOLD_FAILED;
} else if (pindexPrev->GetMedianTimePast() >= nTimeStart) {
stateNext = THRESHOLD_STARTED;
}
if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) {
stateNext = THRESHOLD_FAILED;
break;
}
部署狀態轉換
BIP9部署中定義了所有軟分叉升級的初始狀態均為THRESHOLD_DEFINED
,並定義創始區塊狀態為THRESHOLD_DEFINED
, 另外如果在程式中遇到blockIndex為nullptr
時,均返回THRESHOLD_DEFINED
狀態。
具體轉換過程如下:THRESHOLD_DEFINED
為軟分叉的初始狀態,如果過去中位數時間(MTP)大於nStartTIme,則狀態轉換為THRESHOLD_STARTED
,如果MTP大於等於nTimeout,則狀態轉換成THRESHOLD_FAILED
;如果在一個目標週期(2016個區塊)內贊成升級的區塊數量佔95%以上(大約1915個區塊),則狀態轉換成THRESHOLD_LOCKED_IN
,否則轉換成THRESHOLD_FAILED
;在THRESHOLD_LOCKED_IN
之後的下一個目標週期,狀態轉換成THRESHOLD_ACTIVE
,同時該部署將保持該狀態。
enum ThresholdState {
THRESHOLD_DEFINED,
THRESHOLD_STARTED,
THRESHOLD_LOCKED_IN,
THRESHOLD_ACTIVE,
THRESHOLD_FAILED,
};
業務邏輯
基類AbstractThresholdConditionChecker
定義了通過共識規則檢查BIP9部署的狀態。有如下方法,其中最後兩個方法在基類中實現,子類繼承了該方法的實現:
- Condition(...)檢測一個區塊是否贊成一個軟分叉升級:首先驗證該區塊version是否有效的version格式, 然後檢測該version是否設定了相應個bit位
- BeginTime(...)返回共識規則中的開始投票時間(採用MTP驗證 pindexPrev->GetMedianTimePast() >= nTimeStart)
- EndTime(...)返回共識規則中的設定的過期時間
- Period(...)返回共識規則中的一個目標週期(當前主鏈的目標週期為2016個區塊)
- Threshold(...)返回nRuleChangeActivationThreshold,表示滿足軟分叉升級的最低要求
- GetStateFor(...)在提供共識規則、開始檢索的區塊索引、以及之前快取的狀態資料判斷當前部署的狀態(後面會詳細分析其邏輯)
- GetStateSinceHeightFor(...)函式的作用是查詢從哪個區塊高度開始,該部署的狀態就已經和當前一致
class AbstractThresholdConditionChecker {
protected:
virtual bool Condition(const CBlockIndex *pindex, const Consensus::Params ¶ms) const = 0;
virtual int64_t BeginTime(const Consensus::Params ¶ms) const = 0;
virtual int64_t EndTime(const Consensus::Params ¶ms) const = 0;
virtual int Period(const Consensus::Params ¶ms) const = 0;
virtual int Threshold(const Consensus::Params ¶ms) const = 0;
public:
ThresholdState GetStateFor(const CBlockIndex *pindexPrev, const Consensus::Params ¶ms, ThresholdConditionCache &cache) const;
int GetStateSinceHeightFor(const CBlockIndex *pindexPrev, const Consensus::Params ¶ms, ThresholdConditionCache &cache) const;
};
類VersionBitsConditionChecker
繼承了AbstractThresholdConditionChecker
。實現了:
- BeginTime(const Consensus::Params ¶ms)
- EndTime(const Consensus::Params ¶ms)
- Period(const Consensus::Params ¶ms)
- Threshold(const Consensus::Params ¶ms)
- Condition(const CBlockIndex *pindex, const Consensus::Params ¶ms)
class VersionBitsConditionChecker : public AbstractThresholdConditionChecker {
private:
// maybe: DEPLOYMENT_TESTDUMMY,DEPLOYMENT_CSV,MAX_VERSION_BITS_DEPLOYMENTS
const Consensus::DeploymentPos id;
protected:
int64_t BeginTime(const Consensus::Params ¶ms) const {
return params.vDeployments[id].nStartTime;
}
int64_t EndTime(const Consensus::Params ¶ms) const {
return params.vDeployments[id].nTimeout;
}
int Period(const Consensus::Params ¶ms) const {
return params.nMinerConfirmationWindow;
}
int Threshold(const Consensus::Params ¶ms) const {
return params.nRuleChangeActivationThreshold;
}
bool Condition(const CBlockIndex *pindex, const Consensus::Params ¶ms) const {
return ((
(pindex->nVersion & VERSIONBITS_TOP_MASK) == VERSIONBITS_TOP_BITS) && (pindex->nVersion & Mask(params)) != 0);
}
...
}
另個一重要的類VersionBitsCache
,包括一個方法和一個陣列。該陣列作為記憶體快取使用,該陣列的成員是一個map,當檢查一個BIP9部署的狀態時,如果在檢查過程中判斷出部署狀態,該map會以區塊索引為鍵值,以狀態資訊(int)為值,快取起來,在下次檢查時可以在該區塊位置直接得到其狀態資訊,對程式起到了優化的作用,避免重複的檢索。
struct VersionBitsCache {
ThresholdConditionCache caches[Consensus::MAX_VERSION_BITS_DEPLOYMENTS];
void Clear();
};
typedef std::map<const CBlockIndex *, ThresholdState> ThresholdConditionCache;
另外WarningBitsConditionChecker
類也繼承了AbstractThresholdConditionChecker
類,實現了對未知升級的追蹤與警告。一旦nVersion中有未預料到的位被設定成1,mask將會生成非零的值。當未知升級被檢測到處THRESHOLD_LOCKED_IN
狀態,軟體應該警告使用者即將到來未知的軟分叉。在下一個目標週期,處於THRESHOLD_ACTIVE
狀態是,更應該強調警告使用者。
需要說明的是:未知升級只有處於LOCKED_IN或ACTIVE的條件下才會發出警告
...
WarningBitsConditionChecker checker(bit);
ThresholdState state = checker.GetStateFor(pindex, chainParams.GetConsensus(), warningcache[bit]);
if (state == THRESHOLD_ACTIVE || state == THRESHOLD_LOCKED_IN) {
if (state == THRESHOLD_ACTIVE) {
std::string strWarning =
strprintf(_("Warning: unknown new rules activated (versionbit %i)"), bit);
SetMiscWarning(strWarning);
if (!fWarned) {
AlertNotify(strWarning);
fWarned = true;
}
} else {
warningMessages.push_back(
strprintf("unknown new rules are about to activate (versionbit %i)", bit));
}
}
...
程式碼拆解
- GetAncestor(int height)函式在整個模組中的使用率非常高,其作用就是為了返回指定高度的區塊索引,作用非常簡單但是其程式碼邏輯不太好理解。可以把整個區塊鏈簡單的看成就是一個連結串列結構,為了獲得指定高度的節點資訊,一般通過依次移動指標到指定區塊即可。在該模組中,使用CBlockIndex類中的pskip欄位,配合
GetSkipHeight(int height)
函式,能夠快速定位到指定高度的區塊,優化了執行的效率。CBlockIndex *CBlockIndex::GetAncestor(int height) { if (height > nHeight || height < 0) { return nullptr; } CBlockIndex *pindexWalk = this; int heightWalk = nHeight; while (heightWalk > height) { int heightSkip = GetSkipHeight(heightWalk); int heightSkipPrev = GetSkipHeight(heightWalk - 1); if (pindexWalk->pskip != nullptr && (heightSkip == height || (heightSkip > height && !(heightSkipPrev < heightSkip - 2 && heightSkipPrev >= height)))) { pindexWalk = pindexWalk->pskip; heightWalk = heightSkip; } else { assert(pindexWalk->pprev); pindexWalk = pindexWalk->pprev; heightWalk--; } } return pindexWalk; } static inline int GetSkipHeight(int height) { if (height < 2) { return 0; } return (height & 1) ? InvertLowestOne(InvertLowestOne(height - 1)) + 1 : InvertLowestOne(height); }
- 在整個模組中進行時間比較判斷是都使用了GetMedianTimePast(), 其作用就是找出當前區塊前的10個區塊,排序後,返回第5個元素的nTime
enum { nMedianTimeSpan = 11 }; int64_t GetMedianTimePast() const { int64_t pmedian[nMedianTimeSpan]; int64_t *pbegin = &pmedian[nMedianTimeSpan]; int64_t *pend = &pmedian[nMedianTimeSpan]; const CBlockIndex *pindex = this; for (int i = 0; i < nMedianTimeSpan && pindex; i++, pindex = pindex->pprev) { *(--pbegin) = pindex->GetBlockTime(); } std::sort(pbegin, pend); return pbegin[(pend - pbegin) / 2]; }
邏輯如下:
- 建立包含11個元素的陣列,包括該區塊和之前的10個區塊
- pbegin、pend兩個遊標(陣列遊標)指向陣列末端
- 遍歷11個區塊,pindex遊標不斷地向前移動
- 陣列遊標向前移動,並將pindex獲取的時間戳賦值給陣列
- 對陣列排序(排序的原因是:區塊時間戳是不可靠的欄位,其大小與建立區塊順序可能不一致)
- 11個區塊去中間的元素,也就是陣列下標為5的元素,因為是奇數個元素,所以不用進行判斷下標無效的問題
- GetStateFor(...)函式在整個模組中至關重要,負責獲取BIP9部署的狀態資訊。首先說明的是在一個目標週期之內,一個BIP9部署的狀態是相同的,也就是說部署狀態只會在難度目標發生改變之後才會更新。GetStateFor(...)函式獲取的是上一個目標週期的最後一個區塊的狀態,如果該狀態可以判斷出部署狀態則得出結果,並將結果儲存在
VersionBitsCache
結構體中;如果該狀態已經存在於快取中則直接返回結果;最後如果該區塊無法得出狀態資訊,則會依次尋找(pindexPrev.nHeight - nPeriod)高度的狀態資訊,直到能夠得出結果。如果直到nullptr也沒有,則返回THRESHOLD_DEFINED
。其中比較重要的是,如果一個區塊表明該部署狀態處於THRESHOLD_STARTED
,則會進行更為詳細的判斷,以證明其狀態是否以及失敗或者可以進入LOCKED_IN階段。ThresholdState AbstractThresholdConditionChecker::GetStateFor(...){ ... if (pindexPrev != nullptr) { pindexPrev = pindexPrev->GetAncestor( pindexPrev->nHeight - ((pindexPrev->nHeight + 1) % nPeriod)); } std::vector<const CBlockIndex *> vToCompute; while (cache.count(pindexPrev) == 0) { if (pindexPrev == nullptr) { cache[pindexPrev] = THRESHOLD_DEFINED; break; } if (pindexPrev->GetMedianTimePast() < nTimeStart) { cache[pindexPrev] = THRESHOLD_DEFINED; break; } vToCompute.push_back(pindexPrev); pindexPrev = pindexPrev->GetAncestor(pindexPrev->nHeight - nPeriod); } assert(cache.count(pindexPrev)); ThresholdState state = cache[pindexPrev]; while (!vToCompute.empty()) { ThresholdState stateNext = state; pindexPrev = vToCompute.back(); vToCompute.pop_back(); switch (state) { case THRESHOLD_DEFINED: { if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) { stateNext = THRESHOLD_FAILED; } else if (pindexPrev->GetMedianTimePast() >= nTimeStart) { stateNext = THRESHOLD_STARTED; } break; } case THRESHOLD_STARTED: { if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) { stateNext = THRESHOLD_FAILED; break; } const CBlockIndex *pindexCount = pindexPrev; int count = 0; for (int i = 0; i < nPeriod; i++) { if (Condition(pindexCount, params)) { count++; } pindexCount = pindexCount->pprev; } if (count >= nThreshold) { stateNext = THRESHOLD_LOCKED_IN; } break; } case THRESHOLD_LOCKED_IN: { stateNext = THRESHOLD_ACTIVE; break; } case THRESHOLD_FAILED: case THRESHOLD_ACTIVE: { break; } } cache[pindexPrev] = state = stateNext; } }
舉例說明:
- 針對某個 bit 位的部署,height( 0 -> 2014 )區塊的所有狀態都為THRESHOLD_DEFINED;
- 當父區塊的高度為 2015 時(即當每次獲取本輪第二個區塊時,才會對本輪的第一個塊的狀態進行賦值,然後本輪所有塊的時間都與本輪第一個塊的狀態相同),因為它不在全域性快取中,則進入條件,且它的MTP時間 >= startTime, 將該塊的索引加入臨時集合中,並將指標向前推至上一輪的初始塊(此時這個塊在集合中),進入接下來的條件執行。
- 當父區塊的高度為 4031(即當前的塊為4032時),它不在全域性快取中,進入條件,且它的MTP時間 >= startTime,將該塊的索引加入臨時集合中,並將指標向前推至上一輪的初始塊(此時這個塊在集合中),進入接下來的條件執行。
- 當父區塊的高度為 6047(即當前的塊為6048時),它不在全域性狀態中,進入條件,且它的MTP時間 >= startTime,將該塊的索引加入臨時集合中,並將指標向前推至上一輪的初始塊(此時這個塊在集合中),進入接下來的條件執行。
- 遍歷臨時集合,因為上一輪的撞態為
THRESHOLD_DEFINED
,且本輪初始塊的時間 >= startTime,將本輪的狀態轉換為THRESHOLD_STARTED
;
- 遍歷臨時集合,因為上一輪的撞態為
- 遍歷臨時集合,因為上一輪的狀態為
THRESHOLD_STARTED
,且本輪初始塊的時間 < timeout, 將統計上一輪部署該bit位的區塊個數(即從 2016 ->4031),假設部署的個數超過閾值(95%),將本輪的狀態轉換為LOCKED_IN
;
- 遍歷臨時集合,因為上一輪的狀態為
- 遍歷臨時集合,因為上一輪的狀態為
THRESHOLD_LOCKED_IN
,將本輪的狀態自動切換為THRESHOLD_ACTIVE
。
- 遍歷臨時集合,因為上一輪的狀態為
- 即 2015 -> 4030 之間所有塊的狀態,都與索引為2015 的塊的部署狀態相同。
- 狀態轉換:
THRESHOLD_DEFINED
->THRESHOLD_STARTED
->THRESHOLD_LOCKED_IN
->THRESHOLD_ACTIVE
- 狀態轉換:
- 從0 -> 2015 -> 4031 -> 6047;
- bitcoin 中的版本檢測按照
nMinerConfirmationWindow
為一輪進行檢測,在本輪之間的所有區塊,都與本輪的第一個塊狀態相同。 - 示例:
- GetStateSinceHeightFor()函式獲取本輪狀態開始時的區塊所在高度; 開始這個狀態輪次的第二個區塊的高度(因為每輪塊的狀態更新,都是當計算每輪第二個塊時,才會去計算,然後把計算的結果快取在全域性快取中;因為所有塊的狀態都是根據它的父區塊確定的);
int AbstractThresholdConditionChecker::GetStateSinceHeightFor( const CBlockIndex *pindexPrev, const Consensus::Params ¶ms, ThresholdConditionCache &cache) const { const ThresholdState initialState = GetStateFor(pindexPrev, params, cache); // BIP 9 about state DEFINED: "The genesis block is by definition in this if (initialState == THRESHOLD_DEFINED) { return 0; } const int nPeriod = Period(params); pindexPrev = pindexPrev->GetAncestor(pindexPrev->nHeight - ((pindexPrev->nHeight + 1) % nPeriod)); const CBlockIndex *previousPeriodParent = pindexPrev->GetAncestor(pindexPrev->nHeight - nPeriod); while (previousPeriodParent != nullptr && GetStateFor(previousPeriodParent, params, cache) == initialState) { pindexPrev = previousPeriodParent; previousPeriodParent = pindexPrev->GetAncestor(pindexPrev->nHeight - nPeriod); } // Adjust the result because right now we point to the parent block. return pindexPrev->nHeight + 1; }
邏輯如下:
- 如果其狀態與當前狀態相同則向上一個目標週期尋找
- 當狀態某個輪次的狀態與本輪的狀態不同時,退出上述迴圈,然後返回這種狀態開始時的高度
- 獲取本輪的塊的狀態, 如果為
THRESHOLD_DEFINED
直接返回0 - 獲取本目標週期的初始塊和上一目標週期的初始塊
- 當上一輪的初始塊不為NULL,並且狀態與本輪狀態相同時,進入迴圈邏輯http://www.aibbt.com/a/14564.html
- 獲取本輪的塊的狀態, 如果為
相關文章
- Django(49)drf解析模組原始碼分析Django原始碼
- (一) Mybatis原始碼分析-解析器模組MyBatis原始碼
- btcpool礦池原始碼分析(3)-BlockMaker模組解析TCP原始碼BloC
- btcpool礦池原始碼分析(5)-JobMaker模組解析TCP原始碼
- btcpool礦池原始碼分析(6)-nmcauxmaker模組解析TCP原始碼UX
- btcpool礦池原始碼分析(9)-statshttpd模組解析TCP原始碼httpd
- btcpool礦池原始碼分析(10)-StratumServer模組解析TCP原始碼Server
- btcpool礦池原始碼分析(4)-GbtMaker模組解析TCP原始碼
- btcpool礦池原始碼分析(6)-PoolWatcher模組解析TCP原始碼
- 比特幣挖礦與原始碼解析比特幣原始碼
- 比特幣原始碼分析--RPC比特幣原始碼RPC
- webpack核心模組tapable原始碼解析Web原始碼
- Spark原始碼解析之Storage模組Spark原始碼
- btcpool礦池原始碼分析(7)-sharelogger模組解析TCP原始碼
- 比特幣原始碼分析-網路(一)比特幣原始碼
- 比特幣原始碼分析--埠對映比特幣原始碼
- SOFARegistry 原始碼|資料同步模組解析原始碼
- Zepto原始碼分析之form模組原始碼ORM
- 【Spring原始碼分析】AOP原始碼解析(上篇)Spring原始碼
- 【Spring原始碼分析】AOP原始碼解析(下篇)Spring原始碼
- python2 traceback模組原始碼解析Python原始碼
- QT Widgets模組原始碼解析與技巧QT原始碼
- Swoole 原始碼分析——Client模組之Recv原始碼client
- Swoole 原始碼分析——Client模組之Send原始碼client
- Django(51)drf渲染模組原始碼分析Django原始碼
- 從原始碼分析Node的Cluster模組原始碼
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- ReentrantLock解析及原始碼分析ReentrantLock原始碼
- QT Widgets模組原始碼解析與實踐QT原始碼
- Hadoop2原始碼分析-HDFS核心模組分析Hadoop原始碼
- Framework 原始碼解析知識梳理(5) startService 原始碼分析Framework原始碼
- Swoole 原始碼分析——Client模組之Connect原始碼client
- Swoole 原始碼分析——Server模組之OpenSSL (上)原始碼Server
- JavaScript 模組化及 SeaJs 原始碼分析JavaScriptJS原始碼
- Django(48)drf請求模組原始碼分析Django原始碼
- mybaits原始碼分析--日誌模組(四)AI原始碼
- mybaits原始碼分析--快取模組(六)AI原始碼快取
- mybaits原始碼分析--binding模組(五)AI原始碼