深度解析挖礦的邏輯和技術實現*****
挖礦是作演算法運算的過程,從計算機和程式碼的角度來說,是反覆執行Hash函式並檢測執行結果的具體過程。與討論演算法一樣,挖礦也是在採用POW共識機制前提下討論。
大家已經非常清楚挖礦是由最開始的CPU挖礦,過度到GPU挖礦,最終演化到當前的ASIC(專業礦機)挖礦時代,本篇解析其中的邏輯設計和技術實現。挖礦的演進是硬體的演進過程,同時也是軟體的演進過程,尤其是軟硬體對接協議的改進過程,因此本文直接將與挖礦有關的幾個核心協議作為小標題,一步步深入討論。
在複查文章時我發現“礦工”一詞用的比較模糊,這種情況在英文文獻也差不多,日常交流中一般指擁有挖礦機器的人,本篇著眼於區塊鏈,挖礦的程式或者機器都統稱礦工(Miner)。
MINING
本小節討論挖礦原理,首先解析比特幣區塊頭(Blockheader)結構,我們說挖礦本質是執行Hash函式的過程,而Hash函式是一個單輸入單輸出函式,輸入資料就是這個區塊頭。比特幣區塊頭共6個欄位:
int32_t nVersion; //版本號,4位元組
uint256 hashPrevBlock; //前一個區塊的區塊頭hash值,32位元組
uint256 hashMerkleRoot; //包含進本區塊的所有交易構造的Merkle樹根,32位元組
uint32_t nTime; //Unix時間戳,4位元組
uint32_t nBits; //記錄本區塊難度,4位元組
uint32_t nNonce; //隨機數,4位元組
如上,比特幣每一次挖礦就是對這80個位元組連續進行兩次SHA256運算(SHA256D),運算結果是固定的32位元組(二進位制256位)。
以上6個欄位情況又各不相同,
nVersion,區塊版本號,只有在升級時候才會改變。
hashPrevBlock,由前一個區塊決定。
nBits,由全網決定,每2016個區塊重新調整,調整演算法固定。
因此以上3個欄位可以理解為是固定的,對於每個礦工來說都一樣。礦工可以自由調整的地方是剩下的3個欄位,
nNonce,提供2^32種可能取值
nTime,其實本欄位能提供的值空間非常有限,因為合理的區塊時間有一個範圍,這個範圍是根據前一個區塊時間來定,比前一個區塊時間太早或者太超前都會被其他節點拒絕。值得一提的是,後一個區塊的區塊時間略早於前一個區塊時間,這是允許的。一般來說,礦工會直接使用機器當前時間戳。
hashMerkleRoot,理論上提供2^256種可能,本欄位的變化來自於對包含進區塊的交易進行增刪,或改變順序,或者修改Coinbase交易的輸入欄位。
根據Hash函式特性,這3個欄位中哪怕其中任意1個位的變化,都會導致Hash執行結果巨大變化。在CPU挖礦時代,搜尋空間主要由nNonce提供,進入礦機時代,nNonce提供的4個位元組已經遠遠不夠,搜尋空間轉向hashMerkleRoot。
1. 打包交易,檢索待確認交易記憶體池,選擇包含進區塊的交易。礦工可以任意選擇,甚至可以不選擇(挖空塊),因為每一個區塊有容量限制(當前是1M),所以礦工也不能無限選擇。對於礦工來說,最合理的策略是首先根據手續費對待確認交易集進行排序,然後由高到低儘量納入最多的交易。
2. 構造Coinbase,確定了包含進區塊的交易集後,就可以統計本區塊手續費總額,結合產出規則,礦工可以計算自己本區塊的收益。
3. 構造hashMerkleRoot,對所有交易構造Merkle數。
4. 填充其他欄位,獲得完整區塊頭。
5. Hash運算,對區塊頭進行SHA256D運算。
6. 驗證結果,如果符合難度,則廣播到全網,挖下一個塊;不符合難度則根據一定策略改變以上某個欄位後再進行Hash運算並驗證。
合格的區塊條件如下:
SHA256D(Blockherder) < F(nBits)
其中,SHA256D(Blockherder)就是挖礦結果,F(nBits)是難度對應的目標值,兩者都是256位,都當成大整數處理,直接對比大小以判斷是否符合難度要求。
為了節約區塊鏈儲存空間,將256位的目標值通過一定變換無失真壓縮儲存在32位的nBits欄位裡。具體變換方法為拆分利用nBits的4個位元組,第1個位元組代表右移的位數,用V1表示,後3個位元組記錄值,用V3表示,則有:
此外難度有最低限制,也就是說 有個最大值,比特幣最低難度取值nBits=0x1d00ffff,對應的最大目標值為:0x00000000FFFF0000000000000000000000000000000000000000000000000000
因此挖礦可以形象的類比拋硬幣,好比有256枚硬幣,給定編號1,2,3……256,每進行一次Hash運算,就像拋一次硬幣,256枚硬幣同時丟擲,落地後要求編號前n的所有硬幣全部正面向上。
SETGENERATE
Setgenerate協議介面代表了CPU挖礦時代。
中本聰在論文裡描述了“1 CPU 1 Vote”的理想數字民主理念,在最初版本客戶端就附帶了挖礦功能,客戶端挖礦非常簡單,當然,需要同步資料結束才可以挖礦。現在有很多算力很低的山寨幣還是直接使用客戶端挖礦,有兩種方式可以啟動挖礦:
1) 在配置檔案設定gen=1,然後啟動客戶端,節點將自行啟動挖礦。
2) 客戶端啟動後,利用RPC介面setgenerate控制挖礦。
如果使用經典QT客戶端,點選“幫助”選單,開啟“除錯視窗”,在“控制檯”輸入如下命令:setgenerate true 2,然後回車,客戶端就開始挖礦,後面的數字代表挖礦執行緒數,如果想關閉挖礦,在控制檯使用如下命令:setgenerate false,可以使用getmininginfo命令檢視挖礦情況。
節點挖礦過程也非常簡單:
構造區塊,初始化區塊頭各個欄位,計算Hash並驗證區塊,不合格則nNonce自增,再計算並驗證,如此往復。在CPU挖礦時代,nNonce提供的4位元組搜尋空間完全夠用(4位元組即4G種可能,單核CPU運算SHA256D算力一般是2M左右),其實nNonce只遍歷完兩個位元組就返回去重構塊。
GETWORK
getwork協議代表了GPU挖礦時代,需求主要源於挖礦程式與節點客戶端分離,區塊鏈資料與挖礦部件分離。
使用客戶端節點直接挖礦,需要同步完整區塊鏈,資料和程式緊密結合,也就是說,如果有多臺電腦進行挖礦,需要每臺電腦都單獨同步一份區塊鏈資料。這其實沒有必要,對於礦工來說,最少只需要一個完整節點就可以。而以此同時,GPU挖礦時代的到來,也需要一個協議與客戶端節點互動。
由節點客戶端構造區塊,然後將區塊頭資料交給外部挖礦程式,挖礦程式遍歷nNonce進行挖礦,驗證合格後交付回給節點客戶端,節點客戶端驗證合格後廣播到全網。
如前所述,區塊頭共80個位元組,由於沒有區塊鏈資料和待確認交易池,nVersion,hashPrevBlock,nBits和hashMerkleRoot這4個欄位共72個位元組必須由節點客戶端提供。挖礦程式主要是遞增遍歷nNonce,必要時候可以微調nTime欄位。
對於顯示卡GPU來說,其實不用擔心nNonce的4位元組搜尋空間不足,而且挖礦程式從節點客戶端那裡拿到一份資料後,不應該埋頭工作太久,不然很有可能這個塊已經被其他人挖到,繼續挖只能做無用功,對於比特幣來說,雖然設計為每10分鐘一個區塊,良好的策略也應該在秒級內重新向節點申請新的挖礦資料。對於顯示卡來說,執行SHA256D算力一般介於200M~1G,nNonce提供4G搜尋空間,也就是說再好的顯示卡也能支撐4秒左右,調整一次nTime,又可以再挖4秒,這個時間綽綽有餘。
節點提供RPC介面getwork,該介面有一個可選引數,如果不帶引數,就是申請挖礦資料,如果帶一個引數,就是提交挖到的塊資料。
不帶引數呼叫getwork,返回資料如下:
{
“midstate” : “9226a024e0b77f61d49fd5ffdf828c6b5c4330c61ea2778c606a8e49d4ad8bd6″,
“data” :”00000002e9337bac28ee28a949d2140f9fb0a0ab740acfd739d7bcf67ca31c2301db858ad2ca54d92c8c1cded715922c4df2b07d9f10fa1a6cf3db7e949b320615761ed4581c76f21b12d87500000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000080020000″,
“hash1″ :”00000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000010000″,
“target” : “00000000000000000000000000000000000000000000000075d8120000000000″
}
Data欄位
共128位元組(80區塊頭位元組 + 48補全位元組),因為SHA256將輸入資料切分成固定長度的分片處理,每個切片64位元組,輸入總長度必須是64位元組的整數倍,輸入長度一般不符合要求,則根據一定規則在元資料末端補全資料。其實對於挖礦來說,補全資料是固定不變的,這裡沒必要提供,外部挖礦軟體可以自行補齊。甚至連nNonce欄位都不需要提供,data最少只需要提供前面的76位元組就夠了。nTime欄位也是必不可少的,外部挖礦程式需要參照節點提供的區塊時間來調節nTime。
Target欄位
即當前區塊難度目標值,採用小頭位元組序,需要翻轉才能使用。
其實對於外部挖礦程式來說,有data 和 target這兩個欄位就可以正常挖礦了,不過getwork協議充分考慮各種情況,儘量幫助外部挖礦程式做力所能及的事,提供了兩個額外欄位,data欄位返回完整補全資料也是出於此理念。
Midstate欄位
如上所述,SHA256對輸入資料分片處理,礦工拿到data資料後,第一個分片(頭64位元組)是固定不變的,midstate就是第一個分片的計算結果,節點幫忙計算出來了。
因此,在midstate欄位輔助下,外部挖礦程式甚至只需要44位元組資料就可以正常挖礦:32位元組midstate + 第一個切片餘下的12(76-64)位元組資料。
Hash1欄位
比特幣挖礦每次都需要連續執行兩次SHA256,第一次執行結果32位元組,需要再補充32位元組資料湊足64位元組作為第二次執行SHA256的輸入。hash1就是補全資料,同理,hash1也是固定不變的。
外部挖礦程式挖到合格區塊後再次呼叫getwork介面將修改過的data欄位提交給節點客戶端。節點客戶端要求返回的資料也必須是128位元組。
每次有外部無參呼叫一次getwork時,節點客戶端構造一個新區塊,在返回資料前,都要把新區塊完整儲存在記憶體,並用hashMerkleRoot作為唯一識別符號,節點使用一個Map來存放所有構造的區塊,當下一個塊已經被其他人挖到時,立即清空Map。
getwork收到一個引數後,首先從引數提取hashMerkleRoot,在Map中找出之前儲存的區塊,接著從引數中提取nNonce和nTime填充到區塊的對應欄位,就可以驗證區塊了,如果難度符合要求,說明挖到了一個塊,節點將其廣播到全網。
getwork協議是最早版本挖礦協議,實現了節點和挖礦分離,經典的GPU挖礦驅動cgminer和sgminer,以及cpuminer都是使用getwork協議進行挖礦。getwork + cgminer一直是非常經典的配合,曾經很多新演算法推出時,都快速被移植到cgminer。即便現在,除了BTC和LTC,其他眾多競爭幣都還在使用getwork協議進行挖礦。礦機出現之後,挖礦速度得到極大提高,當前比特幣礦機算力已經達到10T/秒級別。而getwork只給外部挖礦程式提供32位元組共4G的搜尋空間,如果繼續使用getwork協議,礦機需要頻繁呼叫RPC介面,這顯然不可行。如今BTC和LTC節點都已經禁用getwork協議,轉向更新更高效的getblocktemplate協議。
GETBLOCKTEMPLATE
getblocktemplate協議誕生於2012年中葉,此時礦池已經出現。礦池採用getblocktemplate協議與節點客戶端互動,採用stratum協議與礦工互動,這是最典型的礦池搭建模式。
與getwork相比,getblocktemplate協議最大的不同點是:getblocktemplate協議讓礦工自行構造區塊。如此一來,節點和挖礦完全分離。對於getwork來說,區塊鏈是黑暗的,getwork對區塊鏈一無所知,他只知道修改data欄位的4個位元組。對於getblocktemplate來說,整個區塊鏈是透明的,getblocktemplate掌握區塊鏈上與挖礦有關的所有資訊,包括待確認交易池,getblocktemplate可以自己選擇包含進區塊的交易。
getblocktemplate 在被開發出來後並非一成不變,在隨後發行的各個版本客戶端都有所升級改動,主要是增添一些欄位,不過核心理念和核心欄位不變。目前比特幣客戶端返回資料如下,考慮到篇幅限制,交易欄位(transactions)只保留了一筆交易資料,其實根據當前實際情況,待確認交易池實時有上萬筆交易,目前區塊基本都是塞滿的(1M容量限制),加上額外資訊,因此每次呼叫getblocktemplate基本都有1.5M左右返回資料,相對於getwork的幾百個位元組而言,不可同日而語。
Version,Previousblockhash,Bits這三個欄位分別指區塊版本號,前一個區塊Hash,難度,礦工可以直接將數值填充區塊頭對應欄位。
Transactions,交易集合,不但給了每一筆交易的16進位制資料,同時給了hash,交易費等資訊。
Coinbaseaux,如果有想要寫入區塊鏈的資訊,放在這個欄位,類似中本聰的創世塊宣言。
Coinbasevalue,挖下一個塊的最大收益值,包括髮行新幣和交易手續費,如果礦工包含Transactions欄位的所有交易,可以直接使用該值作為coinbase輸出。
Target,區塊難度目標值。
Mintime,指下一個區塊時間戳最小值,Curtime指當前時間,這兩個時間作為礦工調節nTime欄位參考。
Height,下一個區塊難度,目前協議規定要將這個值寫入coinbase的指定位置。
礦工拿到這些資料之後,挖礦步驟如下:
1. 構建coinbase交易,涉及到欄位包括Coinbaseaux,Coinbasevalue,Transactions,Height等,當然最重要的是要指定一個收益地址。
2. 構建hashMerkleRoot,將coinbase放在transactions欄位包含的交易列表之前,然後對相鄰交易兩兩進行SHA256D運算,最終可以構造交易的Merkle樹。由於coinbase有很多位元組可供礦工隨意發揮,此外交易列表也可隨意調換順序或者增刪,因而hashMerkleRoot值空間幾乎可以認為是無限的。其實getblocktemplate協議設計的主要目標就是讓礦工獲得這個巨大的搜尋空間。
3. 構建區塊頭,利用Version,Previousblockhash,Bits以及Curtime分別填充區塊頭對應欄位,nNonce欄位可預設置0。
4. 挖礦,礦工可在由nNonce,nTime,hashMerkleRoot提供的搜尋空間裡設計自己的挖礦策略。
5. 上交資料,當礦工挖到一個塊後當立即使用submitblock介面將區塊完整資料提交給節點客戶端,由節點客戶端驗證並廣播。
需要注意的是,與上文提到的GPU採用getwork挖礦一樣,雖然getblocktemplate給礦工提供了巨大搜索空間,但礦工不應對一份請求資料挖礦太久,而應迴圈適時向節點索要最新區塊和最新交易資訊,以提高挖礦收益。
POOL
挖礦有兩種方式,一種叫SOLO挖礦,另一種是去礦池挖礦。前文所述的在節點客戶端直接啟動CPU挖礦,以及依靠getwork+cgminer驅動顯示卡直接連線節點客戶端挖礦,都是SOLO挖礦,SOLO好比自己獨資買彩票,不輕易中獎,中獎則收益全部歸自己所有。去礦池挖礦好比合買彩票,大家一起出錢,能買一堆彩票,中獎後按出資比率分配收益。理論上,礦機可以藉助getblocktemplate協議連結節點客戶端SOLO挖礦,但其實早已沒有礦工會那麼做,在寫這篇文章時,比特幣全網算力1600P+,而當前最先進的礦機算力10T左右,如此算來,單臺礦機SOLO挖到一個塊的概率不到16萬分之一,礦工(人)投入真金白銀購買礦機、交付電費,不會做風險那麼高的投資,顯然投入礦池抱團挖礦以降低風險,獲得穩定收益更加適合。因此礦池的出現是必然,也不可消除,無論是否破壞系統的去中心化原則。
礦池的核心工作是給礦工分配任務,統計工作量並分發收益。礦池將區塊難度分成很多難度更小的任務下發給礦工計算,礦工完成一個任務後將工作量提交給礦池,叫提交一個share。假如全網區塊難度要求Hash運算結果的前70個位元位都是0,那麼礦池給礦工分配的任務可能只要求前30位是0(根據礦工算力調節),礦工完成指定難度任務後上交share,礦池再檢測在滿足前30位為0的基礎上,看看是否碰巧前70位都是0。
礦池會根據每個礦工的算力情況分配不同難度的任務,礦池是如何判斷礦工算力大小以分配合適的任務難度呢?調節思路和比特幣區塊難度一樣,礦池需要藉助礦工的share率,礦池希望給每個礦工分配的任務都足夠讓礦工運算一定時間,比如說1秒,如果礦工在一秒之內完成了幾次任務,說明礦池當前給到的難度低了,需要調高,反之。如此下來,經過一段時間調節,礦池能給礦工分配合理難度,並計算出礦工的算力。
關於礦池,還有一個小插曲,在礦池剛出現時,反對聲特別強烈,很多人悲觀的認為礦池最終會導致算力集中,危及系統安全,甚至置比特幣於死地。於是有人設計並實現了P2P礦池,力圖將“抱團挖礦”去中心化,程式碼也都是開源的,但由於效率遠不如中心化的礦池沒能吸引太多算力,所謂理想很豐滿,現實很骨感。
推薦幾個比較成熟的開源礦池專案,有興趣的讀者可自行研究:
u PHP-MPOS,早期非常經典的礦池,很穩定,被使用最多,尤其山寨幣礦池,後端使用Stratum Ming協議,原始碼地址https://github.com/MPOS/php-mpos
u node-open-mining-portal,支援多幣種挖礦,原始碼地址https://github.com/zone117x/node-open-mining-portal
u Powerpool,支援混合挖礦,原始碼地址https://github.com/sigwo/powerpool
執行一個礦池需要考慮的問題很多,比如為了得到最及時的全網資訊,礦池一般對接幾個網路節點,而且最好分佈在地球的幾大洲。另外提高出塊率,降低孤塊率,降低空塊率等都是礦池的核心技術問題,本文不能一一展開討論,接下來只詳細討論一個問題,即礦池與礦工的具體配合工作方式——stratum協議。
STRATUM
礦池通過getblocktemplate協議與網路節點互動,以獲得區塊鏈的最新資訊,通過stratum協議與礦工互動。此外,為了讓之前用getwork協議挖礦的軟體也可以連線到礦池挖礦,礦池一般也支援getwork協議,通過階層挖礦代理機制實現(Stratum mining proxy)。須知在礦池剛出現時,顯示卡挖礦還是主力,getwork用起來非常方便,另外早期的FPGA礦機有些是用getwork實現的,stratum與礦池採用TCP方式通訊,資料使用JSON封裝格式。
礦工驅動:在getblocktemplate協議裡,依然是由礦工主動通過HTTP方式呼叫RPC介面向節點申請挖礦資料,這就意味著,網路最新區塊的變動無法及時告知礦工,造成算力損失。
資料負載:如上所述,如今正常的一次getblocktemplate呼叫節點都會反饋回1.5M左右的資料,其中主要資料是交易列表,礦工與礦池需頻繁互動資料,顯然不能每次分配工作都要給礦工附帶那麼多資訊。再者巨大的記憶體需求將大大影響礦機效能,增加成本。
Stratum協議徹底解決了以上問題。
Stratum協議採用主動分配任務的方式,也就是說,礦池任何時候都可以給礦工指派新任務,對於礦工來說,如果收到礦池指派的新任務,應立即無條件轉向新任務;礦工也可以主動跟礦池申請新任務。
現在最核心的問題是如何讓礦工獲得更大的搜尋空間,如果參照getwork協議,僅僅給礦工可以改變nNonce和nTime欄位,則互動的資料量很少,但這點搜尋空間肯定是不夠的。想增加搜尋空間,只能在hashMerkleroot下功夫,如果讓礦工自己構造coinbase,那麼搜尋空間的問題將迎刃而解,但代價是必要要把區塊包含的所有交易都交給礦工,礦工才能構造交易列表的Merkleroot,這對於礦工來說壓力更大,對於礦池頻寬要求也更高。
Stratum協議巧妙解決了這個問題,成功實現既可以給礦工增加足夠的搜尋空間,又只需要互動很少的資料量,這也是Stratum協議最具創新的地方。
Stratum協議嚴格規定了礦工和礦池互動的介面資料結構和互動邏輯,具體如下:
1. 礦工訂閱任務
啟動挖礦機器,使用mining.subscribe方法連結礦池
{“id”: 1, “method”: “mining.subscribe”, “params”: []}\n //申請連結
{“id”: 1, “result”: [ [ ["mining.set_difficulty", "b4b6693b72a50c7116db18d6497cac52"], ["mining.notify", "ae6812eb4cd7735a302a8a9dd95cf71f"]], “08000002″, 4], “error”: null}\n //返回資料
返回資料很重要,礦工需本地記錄,在整個挖礦過程中都用到,其中:
u b4b6693b72a50c7116db18d6497cac52:給礦工指定初始難度,
u ae6812eb4cd7735a302a8a9dd95cf71f:訂閱號ID
u 08000002:學名Extranonce1 ,用於構造coinbase交易
u 4:學名Extranonce2_size ,即Extranonce2的長度,這裡指定4個位元組
Extranonce1,和 Extranonce2對於挖礦很重要,增加的搜尋空間就在這裡,現在,我們至少有了8個位元組的搜尋空間,即nNonce的4個位元組,以及 Extranonce2的4個位元組。
2. 礦池授權
在礦池註冊一個賬號 ,新增礦工,礦池允許每個賬號任意新增礦工數,並取不同名字以區分。礦工使用mining.authorize 方法申請授權,只有被礦池授權的礦工才能收到礦池指派任務。
{“params”: ["slush.miner1", "password"], “id”: 2, “method”: “mining.authorize”}\n
{“error”: null, “id”: 2, “result”: true}\n
3. 礦池分配任務
{“params”: ["bf", "4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000",
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008",
"072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000", ["76cffd68bba7ea661512b68ec6414438191b08aaeaec23608de26ac87820cbd02016","e5a796c0b88fe695949a3e7b0b7b1948a327b2f28c5dbe8f36f0a18f96b2ffef2016"],
“00000002″, “1c2ac4af”, “504e86b9″, false], “id”: null, “method”: “mining.notify”}
以上每個欄位資訊都是必不可少,其中:
u bf:任務號ID,每一次任務都有唯一識別符號
u 4d16b6f85af6e2198f44ae2a6de67f78487ae5611b77c6c0440b921e00000000:前一個區塊hash值,hashPrevBlock
u 01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff20020862062f503253482f04b8864e5008:學名coinb1 ,構造coinbase的第一部分序列資料
u 072f736c7573682f000000000100f2052a010000001976a914d23fcdf86f7e756a64a7a9688ef9903327048ed988ac00000000:學名coinb2 ,構造coinbase的第二部分序列資料,
u ["76cffd68bba7ea661512b68ec6414438191b08aaeaec23608de26ac87820cbd02016","e5a796c0b88fe695949a3e7b0b7b1948a327b2f28c5dbe8f36f0a18f96b2ffef2016"]:學名merkle_branch,交易列表的壓縮表示方式,即上圖的黑點
u 00000002:區塊版本號,nVersion
u 1c2ac4af:區塊難度nBits
u 504e86b9:當前時間戳,nTime
有了以上資訊,再加上之前拿到的Extranonce1 和Extranonce2_size,就可以挖礦了。
4. 挖礦
1) 構造coinbase交易
用到的資訊包括Coinb1, Extranonce1, Extranonce2_size 以及Coinb2,構造很簡單:
Coinbase=Coinb1 + Extranonce1 + Extranonce2 + Coinb2
為啥可以這樣,因為礦池幫礦工做了很多工作,礦池已經構建了coinbase交易,系列化後在指定位置分割成coinb1和coinb2,coinb1和coinb2包含指定資訊,比如coinb1包含區塊高度,coinb2包含了礦工的收益地址和收益額等資訊,但是這些資訊對於礦工來說無關緊要,礦工挖礦的地方只是Extranonce2 的4個位元組。另外Extranonce1是礦池寫入區塊的指定資訊,一般來說,每個礦池會寫入自己礦池的資訊,比如礦池名字或者域名,我們就是根據這個資訊統計每個礦池在全網的算力比重。
2) 構建Merkleroot
利用coinbase和merkle_branch,按照上圖方式構造hashMerkleroot欄位。
3) 構建區塊頭
填充餘下的5個欄位,現在,礦池可以在nNonce和Extranonce2 裡搜尋進行挖礦,如果嫌搜尋空間還不夠,只要增加Extranonce2_size為多幾個位元組就可輕而易舉解決。
5. 礦工提交工作量
當礦工找到一個符合難度的shares時,提交給礦池,提交的資訊量很少,都是必不可少的欄位:
{“params”: ["slush.miner1", "bf", "00000001", "504e86ed", "b2957c02"], “id”: 4, “method”: “mining.submit”}
{“error”: null, “id”: 4, “result”: true}
slush.miner1:礦工名字,礦池用以識別誰提交的工作量
bf:任務號ID,礦池在分配任務之前,構造了Coinbase等資訊,用這個任務號唯一標識
00000001:Extranonce2
504e86ed:nTime欄位
b2957c02:nNonce欄位
礦池拿到以上5個欄位後,首先根據任務號ID找出之前分配任務前儲存的資訊(主要是構建的coinbase交易以及包含的交易列表等),然後重構區塊,再驗證shares難度,對於符合難度要求的shares,再檢測是否符合全網難度。
6. 礦池給礦工調節難度
礦池記錄每個礦工的難度,並根據shares率不斷調節以指定合適難度。礦池可以隨時通過mining.set_difficulty方法給礦工發訊息另其改變難度。
{ “id”: null, “method”: “mining.set_difficulty”, “params”: [2]}
如上,Stratum協議核心理念基本解析清楚,在getblocktemplate協議和Stratum協議的配合下,礦池終於可以大聲的對礦工說,讓算力來的更猛烈些吧。
AUXPOW
在挖礦的發展歷史上,還出現了一個天馬行空的事情,即混合挖礦(Merge Mining)。域名幣(Namecoin)最先使用混合挖礦模式,掛靠在比特幣鏈條上,礦工挖比特幣時,可以同時挖域名幣,後來狗狗幣(Dogecoin)也支援混合挖礦,掛靠在萊特幣(Litecoin)鏈條上。混合挖礦使用Auxiliary Proof-of-Work (AuxPOW)協議實現,雖然混合挖礦不怎麼流行,但是協議設計的很精巧,最初看到協議時我不禁感嘆社群的力量之偉大,這種都能想出來。
以域名幣的混合挖礦舉例,比特幣作為父鏈(Parent Blockchain),域名幣作為輔鏈(Auxiliary Blockchain),AuxPOW協議的實現無需改動父鏈(比特幣當然不會為了域名幣做任何改動),但輔鏈需要做針對性設計,比如狗狗幣改為支援混合挖礦時就進行了硬分叉。
AuxPOW協議核心理念不同的地方在於:
對於經典的POW區塊,規定只有難度符合要求才算一個合格的區塊,AuxPOW協議對區塊難度沒有要求,但附加兩個條件:
1. 輔鏈區塊的hash值必須內置於父鏈區塊的Coinbase裡。
2. 該父鏈區塊的難度必須符合輔鏈的難度要求。
將輔鏈區塊的hash值內置於父鏈的Coinbase,其實是利用父鏈作存在證明。這樣就可以實現間接依靠父鏈的算力來維護輔鏈安全。一般來說,父鏈的算力比輔鏈大,因而滿足父鏈難度要求的區塊一定同時滿足輔鏈難度要求,反之則不成立。這樣一來,很多本來在父鏈達不到難度要求的區塊,卻達到輔鏈難度要求,礦工g=廣播到輔鏈網路,在輔鏈獲得收益,何樂而不為。
AuxPOW協議對兩條鏈都有一些資料結構方面的規定,對於父鏈,要求必須在區塊的coinbase的scriptSig欄位中插入如下格式的44位元組資料:
將輔鏈區塊hash值內建在父鏈的Coinbase,意味著礦工在構造父鏈Coinbase之前,必先構造輔鏈的AuxPOW 區塊並計算hash值。如果只挖一條輔鏈,情況較為簡單,如果同時挖多條輔鏈,則先對所有輔鏈在挖區塊構造Merkleroot。礦池可以將特定的44位元組資訊內置於上文Stratum協議中提到的Coinb1中,交給礦工挖礦。對礦工返回的shares重構父鏈區塊和所有輔鏈區塊,並檢測難度,如果符合輔鏈難度要求,則將整個AuxPOW區塊廣播到輔鏈。
輔鏈節點驗證AuxPOW區塊邏輯過程如下:
1. 依靠父鏈區塊頭(parent_block)和區塊Hash值(block_hash,本欄位其實沒必要,因為節點可以自行計算),驗證父鏈區塊頭是否符合輔鏈難度要求。
2. 依靠Coinbase交易(coinbase_txn)、其所在的分支(coinbase_branch)以及父鏈區塊頭(parent_block),驗證Coinbase交易是否真的被包含在父鏈區塊中。
3. 依靠輔鏈分支(blockchain_branch),以及Coinbase中放Hash值的地方(aux_block_hash),驗證輔鏈區塊Hash是否內置於父鏈區塊的Coinbase交易中。
通過以上3點驗證,則視為合格的輔鏈區塊。
CONCLUSION
中本聰最初設計比特幣時希望所有節點都採用CPU挖礦,一般認為只有這樣才能充分保證區塊鏈的去中心化特徵,比特幣在CPU時代安全度過了萌芽階段。getwork和cgminer將挖礦帶入GPU時代,國內顯示卡曾經一度脫銷,全網算力迅速提升了一個檔次,CPU挖礦慘遭淘汰。隨著越來越多人蔘與挖礦,全網算力不斷上升,催生了抱團挖礦(礦池)。然而GPU時代的繁榮歷史也沒能持續多久就被getblocktemplate,stratum以及礦機帶入了ASIC時代。
getwork實現了資料與挖礦分離,getblocktemplate給外部挖礦程式提供了最大自由度,徹底解決了外部挖礦程式與節點互動的可擴充套件性問題(scalability problems),主要用於礦池與網路節點對接。stratum不但解決了搜尋空間不足的問題,同時也解決了礦池與礦機互動資料量大的問題。getblocktemplate和stratum這兩個協議使大型礦池,大規模礦場,大算力礦機成為可能,從此挖礦產業進入一個全新階段,此後挖礦的演進主要集中於幾個方向:礦池的設計優化與穩定執行,礦場的科學部署,以及礦機工藝升級,提升算力,降低功耗等。
作者:周鄴飛—幣創網技術副總裁、區塊鏈技術專家、DACA區塊鏈協會講師。