比特幣原始碼分析:VersionBits模組解析

aibbtcom發表於2018-02-22

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 &params) const {
    return ((
            (pindex->nVersion & VERSIONBITS_TOP_MASK) ==VERSIONBITS_TOP_BITS) && 
            (pindex->nVersion & Mask(params)) != 0);
}

uint32_t Mask(const Consensus::Params &params) 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,
};

比特幣原始碼分析:VersionBits模組解析

業務邏輯

基類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 &params) const = 0;
    virtual int64_t BeginTime(const Consensus::Params &params) const = 0;
    virtual int64_t EndTime(const Consensus::Params &params) const = 0;
    virtual int Period(const Consensus::Params &params) const = 0;
    virtual int Threshold(const Consensus::Params &params) const = 0;
    

public:
    ThresholdState GetStateFor(const CBlockIndex *pindexPrev, const Consensus::Params &params, ThresholdConditionCache &cache) const;
    int GetStateSinceHeightFor(const CBlockIndex *pindexPrev, const Consensus::Params &params, ThresholdConditionCache &cache) const;
};

VersionBitsConditionChecker繼承了AbstractThresholdConditionChecker。實現了:

  • BeginTime(const Consensus::Params &params)
  • EndTime(const Consensus::Params &params)
  • Period(const Consensus::Params &params)
  • Threshold(const Consensus::Params &params)
  • Condition(const CBlockIndex *pindex, const Consensus::Params &params)
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 &params) const {
        return params.vDeployments[id].nStartTime;
    }
    int64_t EndTime(const Consensus::Params &params) const {
        return params.vDeployments[id].nTimeout;
    }
    int Period(const Consensus::Params &params) const {
        return params.nMinerConfirmationWindow;
    }
    int Threshold(const Consensus::Params &params) const {
        return params.nRuleChangeActivationThreshold;
    }

    bool Condition(const CBlockIndex *pindex, const Consensus::Params &params) 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));
    }
}
...

程式碼拆解

  1. 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);
    }
  2. 在整個模組中進行時間比較判斷是都使用了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的元素,因為是奇數個元素,所以不用進行判斷下標無效的問題
  3. 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 為一輪進行檢測,在本輪之間的所有區塊,都與本輪的第一個塊狀態相同。
    • 示例:
  4. GetStateSinceHeightFor()函式獲取本輪狀態開始時的區塊所在高度; 開始這個狀態輪次的第二個區塊的高度(因為每輪塊的狀態更新,都是當計算每輪第二個塊時,才會去計算,然後把計算的結果快取在全域性快取中;因為所有塊的狀態都是根據它的父區塊確定的);
    int AbstractThresholdConditionChecker::GetStateSinceHeightFor(
        const CBlockIndex *pindexPrev, const Consensus::Params &params,
        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

相關文章