比特幣挖礦難度精講
比特幣主鏈上,平均每十分鐘會出一個塊。隨著數字貨幣的發展,參與的 miner 數量與日俱增,挖礦技術日新月異,全網的算力也是以驚人的速度增長。BTC 中為了保證主鏈平均的高度增加速度依然維持最初設定,進而設定了挖礦難度調整的功能。深入理解挖礦難度的概念,以及挖礦難度調整的方案,對開發人員以及 miner 都很重要,因為挖礦難度設定不合理可能會導致全網出塊速度極不穩定。本文將詳細介紹 BTC&BCH 挖礦難度及調整方案,我們先從 PoW 演算法講起。
1、PoW 演算法
PoW (Proof-of-Work)工作量證明演算法是一種對應服務與資源濫用、或是阻斷服務攻擊的經濟對策。一般是要求使用者進行一些耗時適當的複雜運算,並且答案能被服務方快速驗算,以此耗用的時間、裝置與能源做為擔保成本,以確保服務與資源是被真正的需求所使用。
PoW 演算法具有:去中心化,單向,隨機性,目標難度易調整等特點,所以現在包括 BTC,BCH 在內的很多幣種都採用了 PoW 共識機制。
從實現上來說,PoW 演算法的輸入為任意長度,輸出為固定長度,比如通常使用 SHA256 演算法對應輸出 256-bit。在挖礦過程中 miner 用 PoW 演算法計算整個塊頭的 hash 值,由於 SHA256 的特性:塊頭任意一位發生變化,得到的 hash 值會變得完全不一樣,而且大小變化方向不確定。於是,我們比較 hash 值是否小於某個值(實際上這個值是儲存在塊頭中的 nBit「解壓後」的 current_target 值)來判斷是否滿足要求;如果小於,則廣播這個區塊;如果不小於,則按照當前挖礦節點的規則改變塊頭中可以改變的值,然後再次計算塊頭 hash 值,以此往復,直到結果小於目標值。
由此可知,current_target 值越小,滿足挖礦要求的概率就越小,挖礦難度就越大。
2、塊頭 & Coinbase 交易
塊頭的生成: 當 miner 開始新一輪打包之後,首先會建立一個空的塊,塊結構分為塊頭以及塊資訊兩部分。先打包塊資訊,再根據塊資訊填充塊頭。首先看一下已經成功打包的塊。下圖是寫本文稿時擷取最新的 BTC block 詳情。
塊資訊 存放的是從 mempool 裡取出來的一系列交易信息,miner 並以此建立了一個 Merkle Tree,交易資訊的 hash 值作為 leaf,最終生成的 Merkle Root 將填到塊頭裡。值得注意的是,交易列表中的第一個是一個非常獨特的交易:Coinbase Transaction。Coinbase Transaction 與普通交易主要的區別有:
1) Coinbase Transaction 不消耗 UTXO
2) input 只有一個,叫做 Coinbase
3) output 的 addresss 為 miner 的 btc/bch 地址
4) value 由挖礦獎勵和交易費組成
5)更值得注意的是 input 中沒有 Unlocking-script,取而代之的是 Coinbase Data (這部分資料包含 Extra Nonce,在挖礦難度非常高時,將起非常重要的作用)
Coinbase 交易 input 的結構如下:
Coinbase data,該欄位資料長度範圍為 2-byte~100-byte:
block height 起初 Coinbase 是不包含塊高度資訊,由於重複交易的問題出現,誕⽣了 BIP30,隨後第二套解決方案 BIP34)。BIP34 規定 Coinbase data 最高位元組表示用於表示塊高度的資料段的位元組數,接下來的位元組以⼩端法表示具體的塊高度,創世塊的高度為 0。
例如:2013-12-28 BTC 的一個塊的 Coinbase 解析中 coinbase data 為 0x03443b04...,則塊高度用 16 進製表示為 0x043b44,十進位制為 277316;
extra nonce 作為中間欄位,將會在後續提及的 Extra Nonce Solution 詳細說明作用;
上圖 Coinbase data 中用以結尾的「/P2SH/」是 12 年 miner 進行投票支援 BTC 是採用 BIP16 還是 BIP17 的產物,現已棄用。(眾所周知,BIP16 P2SH 獲得了更多票數,被 BTC 採用)
在交易資訊聚合完畢得到了 MerkleRoot 之後,接下來填充區塊頭。塊頭結構如下(其中 nBit 就是 PoW 小節提到的 current-target 的壓縮版):
區塊頭 80-byte,一共 6 個欄位:
- 版本號,允許改變但不推薦
- 前一個塊的 hash 值,不允許改變
- MerkleRoot 的 hash 值,用於存塊資訊裡的交易的 Merkle Tree 的 root 節點的值,允許改變(改變 coinbase 中 input 中的值)
- 時間戳,允許基於 MTP11 進行調整改變
- nonce,用於 PoW 演算法的隨機值,允許改變
- nBit,PoW 演算法結果必須小於這個數對應的 current_target 才能算塊打包成功。這個值是在每一個塊開始打包之前就確定了,不允許改變
塊頭中 80 bytes 任意一個值發生改變,PoW 的 Hash 結果就會發生改變。
3、挖礦難度及難度調整
理解了 PoW 運算以及運算結果可能受哪些個因素影響,接下來了解一下我們要滿足的難度要求是什麼。
挖礦難度的描述可以認為有三種形式,difficulty (難度值,浮點數),current_target (當前目標值,256-bit),nbits (32-bit);形式不同其實實質是表達的同一個難度要求,而且這個難度要求在每個塊打包前就確定了。
difficulty 不寫在區塊中,而是以浮點數的形式展現,給人直觀的感受難度程度。 difficulty = difficulty_1 / current_target;
difficulty_1 為常數:0x00000000ffff0000000000000000000000000000000000000000000000000000
創世塊的 current_target = difficulty_1,所以創世塊的 difficulty = 1.0。
nbits 就是區塊頭 nBits 欄位的值,用長度為 32-bit 的數值表示 256-bit 的數值,是需要犧牲一定精度的 , 可以理解它為「壓縮」後的 current_target。
在計算 current_target 時,我們先轉換為二進位制然後用公式(a)來計算 256-bit 的 current_target。(值得注意的是:current_target 是一個無符號 256-bit 的值,之所以設定一個 Sign 欄位,是為了與 bitcoind 程式碼保持一致,保留符號位參考的是 IEEE 浮點型表示法,其實是無用的)
This compact form is only used in bitcoin to encode unsigned 256-bit numbers which represent difficulty targets, thus there really is not a need for a sign bit, but it is implemented here to stay consistent with bitcoind.
計算 current_target 的公式為:
例如 nBits = 0x180192d4,
current_target = 0x192d4 * 2 ^ {(8 * (0x18 - 3))}
(最高 16 位為零)
相較於創世塊,current_target 減小了大約 1/(236) 倍,difficulty 增加了大約 7184404942701 倍。
為了維持平均每十分鐘生成一個塊的頻率,BTC 中 current_target 設計為了一個動態值,current_target 根據全網算力的改變而做一些相應的調整,這就是挖礦難度調整。
在 BTC 中,挖礦難度調整 idea 為:以 2016 個塊(兩週)為一個週期,每個週期根據前一個週期的實際耗時與理論耗時之間的差別進行調整。
新目標值 = 當前目標值 * 實際 2016 個區塊出塊時間 / 理論 2016 個區塊出塊時間 (2 周)。
方案具體邏輯是:
- 判斷是否需要更新目標值 ( 2016 的整數倍),如果不是則繼續使用最後一個區塊的目標值
- 計算 2016 個塊實際使用時長:如果用時低於半周,則按半周計算,防止難度增加 4 倍以上;如果用時高於 8 周,則按 8 周計算。防止難度降低到 4 倍以下。
- 實際使用時長乘以當前難度,再除以 2 周
- 如果超過最大難度限制,則按最大難度處理
計算過程,Go 程式碼如下 :
4、BCH 難度調整
BCH 誕生於區塊高度 478558,兩條鏈都採用相同 PoW 共識演算法(平均 10 分鐘生成一個塊),所以 miner 可以任意在 BTC 與 BCH 間切換,但由於通常 BCH 全網算力只佔有 BTC 的 7% 左右,當 BCH 獲利大於 BTC 的時候,大量原 BTC miner (尤其是大的礦場)會切入 BCH,一段時間後隨著算力提升,難度值也會提升,miner 會紛紛離開切回 BTC,算力降低,難度居高不下將導致接下來出塊十分困難。倘若繼續沿用 BTC 的難度調整方案,BCH 將無法保證出塊速率穩定在平均 10mins/block,事實上 BCH 的難度值調整演算法已經先後經歷了兩種,第一種是緊急難度調整規則(EDA),目前使用的是難度調整規則(DAA)。
緊急難度調整規則(EDA)
EDA 是在沿用 BTC 難度調整演算法的基礎上,增加了一個 Emergency Difficulty Adjustment 處理方案,主要是針對於出塊緩慢情況及時降低難度。演算法具體邏輯是:對於高度為 2016 倍數的就拿到此高度前 2016 塊的 blocktime,沿用 BTC 難度調整方案;對於高度非 2016 倍數的塊,則計算生成前六塊的塊總共耗時是否超過 12h,如果超過則降低挖礦難度 20%。 具體實現程式碼如下:
難度調整規則(DAA)
然而在執行三個多月中,EDA 表現不盡如人意(非常差),由於其應對出塊速率過高並沒有做相應及時調整而依賴於 BTC 原有的 2016 塊一次的調整機制,所以在面對算力波動時表現並不是很好。
本人分別截取了三段「典型」資料,為了對上述觀點進行說明:
- 2017.8.26 號的算力攻擊,2017.8.27 號大算力切走之後出塊困難,23 小時只出 13 個塊,導致連續調整(降低)挖礦難度。
- 2017.10.2 由於礦工趨利性導致 BCH 網路算力突然增加,僅僅 30mins 就出了 20 多區塊。
- 用一個更直觀的資料,BTC 和 BCH 的起跑線都是一樣的,都是 2017 年 8 月 1 日,高度同為 478,558,而截止到 17 年 11 月 12 日晚 BTC 挖到了 494,079 高度,而 BCH 挖到了 503,815 高度,多了將近 10000 個塊。
所以優化 BCH 的難度調整方案刻不容緩,在得到幾大礦池的穩定算力支援後,17 年 11 月 13 日,BCH 再次升級,就是為了優化 EDA。BCH 開發團隊(並非社群)收到幾份 DAA,最終採用了 BTCABC 開發團隊 Amaury Sechet 的 DAA 提案。 這份 proposal 的 ieda 可以用一句俗語來形容「魔高一尺,道高一尺,魔有天花板」,根據前一天的算力為基準從而預測需要設定多少工作量才能耗掉十分鐘。 其實現邏輯如下:
- 新演算法將在高度 504031 開始生效
- 假設需要得到 target_height 的目標難度
- (prevBlock - 1)至 (prevBlock - 1 - 2 )這三塊的 ntime,排序,取 ntime 在中間的那塊為 lastNode
- 取(prevBlock - 1 - 144)至 (prevBlock - 1 - 144 - 2)這三塊的 ntime,排序,取 ntime 在中間的那塊為 firstNode 備註:bch 的目標是 10 分鐘產生一塊,一天產生 144 塊
- 根據最近的 144 個區塊的鏈上累計工作量(ChainWork)可以推算出滿足當前算力的所需工作量 work :
- 再通過 work 值得出目標難度。
具體實現程式碼如下:
計算新 Target 方法如下:
總結下來,DAA 演算法具有以下特性:
- 基於前 144 個塊的算力來逐塊設定挖礦難度;
- 算力按指數規律變化時,網路將快速調整難度,保證公平性;
- 避免當前算力與目標難度的不匹配導致的反饋振盪。
- 可以一定程度上減少 timestamp manipulation 等攻擊的影響。
DAA 應對算力攻擊的效果如何呢?
以下是兩個算力變化極端場景:算力陡增兩倍,算力陡然減半。 poc 程式碼如下:
5、extra nonce 解決方案
目前,BTC 挖礦難度設定到了 7184404942701.792( 0x17272d92 對應的 current_target 為 0x000000000000000000272d920000000000000000000000000000000000000000)。 也就是說隨機選取的數滿足小於 target 的概率是 1/(272),但是塊頭的 nonce 欄位只有 4bytes,也就是 32 位,有可能 232 個隨機數都試完來仍然找不到滿足 target 的 result。所以允許塊頭內部分其他的欄位改變,用來生成新的 result。允許改變的欄位在第二小節塊頭部分已指明。
試想一下,如果頻繁更改 Coinbase Data 裡的 Extra Nonce,來改變塊頭的 Merkle Root 會怎麼樣?很明顯效率會很低,所以實際挖礦中策略是:儘可能減少塊頭中 Version,TimeStamp,Merkle Root (綠色區域資料)值的改變,而「瘋狂」遍歷 Nonce (紅色區域)的值用於 PoW;當遍歷完沒找到滿足 target 的 result,再改變綠色區域的值,然後繼續「瘋狂」遍歷 Nonce。如此往復直至找到滿足 target 的 result 或者這一輪 PoW 競賽中失敗開始新一輪打包。