比特幣學習筆記——————8、挖礦與共識

FLy_鵬程萬里發表於2018-06-12


8.1 簡介

挖礦是增加比特幣貨幣供應的一個過程。挖礦同時還保護著比特幣系統的安全,防止欺詐交易,避免“雙重支付”,“雙重支付”是指多次花費同一筆比特幣。礦工們通過為比特幣網路提供算力來換取獲得比特幣獎勵的機會。

礦工們驗證每筆新的交易並把它們記錄在總帳簿上。每10分鐘就會有一個新的區塊被“挖掘”出來,每個區塊裡包含著從上一個區塊產生到目前這段時間內發生的所有交易,這些交易被依次新增到區塊鏈中。我們把包含在區塊內且被新增到區塊鏈上的交易稱為“確認”交易,交易經過“確認”之後,新的擁有者才能夠花費他在交易中得到的比特幣。

礦工們在挖礦過程中會得到兩種型別的獎勵:建立新區塊的新幣獎勵,以及區塊中所含交易的交易費。為了得到這些獎勵,礦工們爭相完成一種基於加密雜湊演算法的數學難題,這些難題的答案包括在新區塊中,作為礦工的計算工作量的證明,被稱為”“工作量證明”。該演算法的競爭的機制以及獲勝者有權在區塊鏈上進行交易記錄的機制,這二者是比特幣安全的基石。

新比特幣的生成過程被稱為挖礦是因為它的獎勵機制被設計為速度遞減模式,類似於貴重金屬的挖礦過程。比特幣的貨幣是通過挖礦發行的,類似於中央銀行通過印刷銀行紙幣來發行貨幣。礦工通過創造一個新區塊得到的比特幣數量大約每四年(或準確說是每210,000個塊)減少一半。開始時為2009年1月每個區塊獎勵50個比特幣,然後到2012年11月減半為每個區塊獎勵25個比特幣。之後將在2016年的某個時刻再次減半為每個新區塊獎勵12.5個比特幣。基於這個公式,比特幣挖礦獎勵以指數方式遞減,直到2140年。屆時所有的比特幣(20,999,999.98)全部發行完畢。換句話說在2140年之後,不會再有新的比特幣產生。

礦工們同時也會獲取交易費。每筆交易都可能包含一筆交易費,交易費是每筆交易記錄的輸入和輸出的差額。在挖礦過程中成功“挖出”新區塊的礦工可以得到該區塊中包含的所有交易“小費”。目前,這筆費用佔礦工收入的0.5%或更少,大部分收益仍來自挖礦所得的比特幣獎勵。然而隨著挖礦獎勵的遞減,以及每個區塊中包含的交易數量增加,交易費在礦工收益中所佔的比重將會逐漸增加。在2140年之後,所有的礦工收益都將由交易費構成。

“挖礦”這個詞有一定的誤導性。它容易引起對貴重金屬採礦的的聯想,從而使我們的注意力都集中在每個新區塊產生的獎勵上。儘管挖礦帶來的獎勵是一種激勵,但它最主要的目的並不是獎勵本身或者新幣的產生。如果只把挖礦看作生產新幣的過程,那你是把手段(激勵措施)當成了目的。挖礦是一種將結算所去中心化的過程,每個結算所對處理的交易進行驗證和結算。挖礦保護了比特幣系統的安全,並且實現了在沒有中心機構的情況下,也能使整個比特幣網路達成共識。

挖礦這個發明使比特幣變得很特別,這種去中心化的安全機制是點對點的電子貨幣的基礎。鑄造新幣的獎勵和交易費是一種激勵機制,它可以調節礦工行為和網路安全,同時又完成了比特幣的貨幣發行。

在本章中,我們先來審視比特幣的貨幣發行機制,然後再來了解挖礦的最重要的功能:支撐比特幣安全的去中心化的自發共識機制。

8.1.1 比特幣經濟學和貨幣創造

通過創造出新區塊,比特幣以一個確定的但不斷減慢的速率被鑄造出來。大約每十分鐘產生一個新區塊,每一個新區塊都伴隨著一定數量從無到有的全新比特幣。每開採210,000個塊,大約耗時4年,貨幣發行速率降低50%。在比特幣執行的第一個四年中,每個區塊創造出50個新比特幣。

2012年11月,比特幣的新發行速度降低到每區塊25個比特幣,並且預計會在2016年的某個時刻,在第420,000個區塊被“挖掘”出來之後降低到12.5比特幣/區塊。在第13,230,000個區塊(大概在2137年被挖出)之前,新幣的發行速度會以指數形式進行64次“二等分”。到那時每區塊發行比特幣數量變為比特幣的最小貨幣單位——1聰。最終,在經過1,344萬個區塊之後,所有的共20,999,999.9769聰比特幣將全部發行完畢。換句話說,到2140年左右,會存在接近2,100萬比特幣。在那之後,新的區塊不再包含比特幣獎勵,礦工的收益全部來自交易費。圖8-1展示了在發行速度不斷降低的情況下,比特幣總流通量與時間的關係。

在例8-1的程式碼展示中,我們計算了比特幣的總髮行量。

例8-1 比特幣發行總量的計算指令碼

# 初始的塊獎勵為50BTC
start_block_reward = 50
# 以10分鐘為一個區塊的間隔,210000個塊共約4年時間
reward_interval = 210000

def max_money():
    # 50 BTC = 50 0000 0000 Satoshis
    current_reward = 50 * 10**8
    total = 0
    while current_reward > 0:
        total += reward_interval * current_reward
        current_reward /= 2
    return total

print "Total BTC to ever be created:", max_money(), "Satoshis"

例8-2顯示了這個指令碼的執行結果。

例8-2 執行 max_money.py 指令碼

$ python max_money.py
Total BTC to ever be created: 2099999997690000 Satoshis



圖8-1 比特幣貨幣供應速度隨著時間發生幾何級降低

總量有限並且發行速度遞減創造了一種抗通脹的貨幣供應模式。法幣可被中央銀行無限制地印刷出來,而比特幣永遠不會因超額印發而出現通脹。

通貨緊縮貨幣

最重要並且最有爭議的一個結論是一種事先確定的發行速率遞減的貨幣發行模式會導致貨幣通貨緊縮(簡稱通縮)。通縮是一種由於貨幣的供應和需求不匹配導致的貨幣增值的現象。它與通脹相反,價格通縮意味著貨幣隨著時間有越來越強的購買力。

許多經濟學家提出通縮經濟是一種無論如何都要避免的災難型經濟。因為在快速通縮時期,人們預期著商品價格會下跌,人們將會儲存貨幣,避免花掉它。這種現象充斥了日本經濟“失去的十年”,就是因為在需求坍塌之後導致了滯漲狀態。

比特幣專家們認為通縮本身並不壞。更確切地說,我們將通縮與需求坍塌聯絡在一起是因為過去出現的一個特例。在法幣屆,貨幣是有可能被無限制印刷出來的,除非遇到需求完全崩塌並且毫無發行貨幣意願的情形,因此經濟很難進入滯漲期。而比特幣的通縮並不是需求坍塌引起的,它遵循一種預定且有節制的貨幣供應模型。

實際上,通縮貨幣會讓賣家考慮到折現的影響,容易誘發過度的囤積本能,除非這部分折現率超過買家的囤積本能。因為買賣雙方都有囤積的動機,這兩種折現率會因為雙方的囤積本能相互抵消,而達成一個平衡價格。因此即使在比特幣價格貼現率為30%的情況下,大部分使用比特幣的零售商並不會感受到花費比特幣很困難,也能因此盈利。當然,比特幣這種不是因經濟快速衰退而引起的通縮,是否會引發其他問題,仍有待觀察。

8.2 去中心化共識

在上一章中我們瞭解了區塊鏈。可以將區塊鏈看作一本記錄所有交易的公開總帳簿(列表),比特幣網路中的每個參與者都把它看作一本所有權的權威記錄。

但在不考慮相信任何人的情況下,比特幣網路中的所有參與者如何達成對任意一個所有權的共識呢?所有的傳統支付系統都依賴於一箇中心認證機構,依靠中心機構提供的結算服務來驗證並處理所有的交易。比特幣沒有中心機構,幾乎所有的完整節點都有一份公共總帳的備份,這份總帳可以被視為認證過的記錄。區塊鏈並不是由一箇中心機構創造的,它是由比特幣網路中的所有節點各自獨立競爭完成的。換句話說比特幣網路中的所有節點,依靠著節點間的不穩定的網路連線所傳輸的資訊,最終得出同樣的結果並維護了同一個公共總帳。這一章將介紹比特幣網路不依靠中心機構而達成共識的機制。

中本聰的主要發明就是這種去中心化的自發共識機制。這種自發,是指沒有經過明確選舉或者沒有固定達成的共識的時間。換句話說,共識是數以千計的獨立節點遵守了簡單的規則通過非同步互動自發形成的產物。所有的比特幣屬性,包括貨幣、交易、支付以及不依靠中心機構和信任的安全模型等都是這個機制的衍生物。比特幣的去中心化共識由所有網路節點的4種獨立過程相互作用而產生:

▷ 每個全節點依據綜合標準對每個交易進行獨立驗證

▷ 通過完成工作量證明演算法的驗算,挖礦節點將交易記錄獨立打包進新區塊,

▷ 每個節點獨立的對新區塊進行校驗並組裝進區塊鏈

▷ 每個節點對區塊鏈進行獨立選擇,在工作量證明機制下選擇累計工作量最大的區塊鏈

在接下來的幾節中,我們將審視這些過程,瞭解它們之間如何相互作用並達成全網的自發共識,從而使任意節點組合出它自己的權威、可信、公開的總帳。

8.3 交易的獨立校驗

在第5章中,我們知道了錢包軟體通過收集UTXO、提供正確的解鎖指令碼、構造支付給接收者的輸出這一系列的方式來建立交易。產生的交易隨後將被髮送到比特幣網路臨近的節點,從而使得該交易能夠在整個比特幣網路中傳播。

然而,在交易傳遞到臨近的節點前,每一個收到交易的比特幣節點將會首先驗證該交易,這將確保只有有效的交易才會在網路中傳播,而無效的交易將會在第一個節點處被廢棄。

每一個節點在校驗每一筆交易時,都需要對照一個長長的標準列表:

▷交易的語法和資料結構必須正確。

▷輸入與輸出列表都不能為空。

▷交易的位元組大小是小於MAX_BLOCK_SIZE的。

▷每一個輸出值,以及總量,必須在規定值的範圍內 (小於2,100萬個幣,大於0)。

▷沒有雜湊等於0,N等於-1的輸入(coinbase交易不應當被中繼)。

▷nLockTime是小於或等於INT_MAX的。

▷交易的位元組大小是大於或等於100的。

▷交易中的簽名數量應小於簽名運算元量上限。

▷解鎖指令碼(scriptSig)只能夠將數字壓入棧中,並且鎖定指令碼(scriptPubkey)必須要符合isStandard的格式 (該格式將會拒絕非標準交易)。

▷池中或位於主分支區塊中的一個匹配交易必須是存在的。

▷對於每一個輸入,如果引用的輸出存在於池中任何的交易,該交易將被拒絕。

▷對於每一個輸入,在主分支和交易池中尋找引用的輸出交易。如果輸出交易缺少任何一個輸入,該交易將成為一個孤立的交易。如果與其匹配的交易還沒有出現在池中,那麼將被加入到孤立交易池中。 
▷對於每一個輸入,如果引用的輸出交易是一個coinbase輸出,該輸入必須至少獲得COINBASE_MATURITY (100)個確認。 
▷對於每一個輸入,引用的輸出是必須存在的,並且沒有被花費。 
▷使用引用的輸出交易獲得輸入值,並檢查每一個輸入值和總值是否在規定值的範圍內 (小於2100萬個幣,大於0)。 
▷如果輸入值的總和小於輸出值的總和,交易將被中止。 
▷如果交易費用太低以至於無法進入一個空的區塊,交易將被拒絕。 
▷每一個輸入的解鎖指令碼必須依據相應輸出的鎖定指令碼來驗證。

這些條件能夠在比特幣標準客戶端下的AcceptToMemoryPoolCheckTransactionCheckInputs函式中獲得更詳細的闡述。請注意,這些條件會隨著時間發生變化,為了處理新型拒絕服務攻擊,有時候也為交易型別多樣化而放寬規則。

在收到交易後,,每一個節點都會在全網廣播前對這些交易進行校驗,並以接收時的相應順序,為有效的新交易建立一個池(交易池)。

8.4 挖礦節點

在比特幣網路中,一些節點被稱為專業節點礦工。第1章中,我們介紹了Jing,在中國上海的計算機工程專業學生,他就是一位礦工。Jing通過礦機挖礦獲得比特幣,礦機是專門設計用於挖比特幣的計算機硬體系統。Jing的這臺專業挖礦裝置連線著一個執行完整比特幣節點的伺服器。與Jing不同,一些礦工是在沒有完整節點的條件下進行挖礦,正如我們在“8.11.2 礦池”一節中所述的。與其他任一完整節點相同,Jing的節點在比特幣網路中進行接收和傳播未確認交易記錄。然而,Jing的節點也能夠在新區塊中整合這些交易記錄。

同其他節點一樣,Jing的節點時刻監聽著傳播到比特幣網路的新區塊。而這些新加入的區塊對挖礦節點有著特殊的意義。礦工間的競爭以新區塊的傳播而結束,如同宣佈誰是最後的贏家。對於礦工們來說,獲得一個新區塊意味著某個參與者贏了,而他們則輸了這場競爭。然而,一輪競爭的結束也代表著下一輪競爭的開始。新區塊並不僅僅是象徵著競賽結束的方格旗;它也是下一個區塊競賽的發令槍。

8.5 整合交易至區塊

驗證交易後,比特幣節點會將這些交易新增到自己的記憶體池中。記憶體池也稱作交易池,用來暫存尚未被加入到區塊的交易記錄。與其他節點一樣,Jing的節點會收集、驗證並中繼新的交易。而與其他節點不同的是,Jing的節點會把這些交易整合到一個候選區塊中。

讓我們繼續跟進,看下Alice從Bob咖啡店購買咖啡時產生的那個區塊(參見“2.1.2 買咖啡”)。Alice的交易在區塊277,316。為了演示本章中提到的概念,我們假設這個區塊是由Jing的挖礦系統挖出的,並且繼續跟進Alice的交易,因為這個交易已經成為了新區塊的一部分。

Jing的挖礦節點維護了一個區塊鏈的本地副本,包含了自2009年比特幣系統啟動執行以來的全部區塊。當Alice買咖啡的時候,Jing節點的區塊鏈已經收集到了區塊277,314,並繼續監聽著網路上的交易,在嘗試挖掘新區塊的同時,也監聽著由其他節點發現的區塊。當Jing的節點在挖礦時,它從比特幣網路收到了區塊277,315。這個區塊的到來標誌著終結了產出區塊277,315競賽,與此同時也是產出區塊277,316競賽的開始。

在上一個10分鐘內,當Jing的節點正在尋找區塊277,315的解的同時,它也在收集交易記錄為下一個區塊做準備。目前它已經收到了幾百筆交易記錄,並將它們放進了記憶體池。直到接收並驗證區塊277,315後,Jing的節點會檢查記憶體池中的全部交易,並移除已經在區塊277,315中出現過的交易記錄,確保任何留在記憶體池中的交易都是未確認的,等待被記錄到新區塊中。

Jing的節點立刻構建一個新的空區塊,做為區塊277,316的候選區塊。稱作候選區塊是因為它還沒有包含有效的工作量證明,不是一個有效的區塊,而只有在礦工成功找到一個工作量證明解之後,這個區塊才生效。

8.5.1 交易塊齡,礦工費和優先順序

Jing的比特幣節點需要為記憶體池中的每筆交易分配一個優先順序,並選擇較高優先順序的交易記錄來構建候選區塊。交易的優先順序是由交易輸入所花費的UTXO的“塊齡”決定,交易輸入值高、“塊齡”大的交易比那些新的、輸入值小的交易擁有更高的優先順序。如果區塊中有足夠的空間,高優先順序的交易行為將不需要礦工費。

交易的優先順序是通過輸入值和輸入的“塊齡”乘積之和除以交易的總長度得到的:

Priority = Sum (Value of input * Input Age) / Transaction Size

在這個等式中,交易輸入的值是由比特幣單位“聰”(1億分之1個比特幣)來表示的。UTXO的“塊齡”是自該UTXO被記錄到區塊鏈為止所經歷過的區塊數,即這個UTXO在區塊鏈中的深度。交易記錄的大小由位元組來表示。

一個交易想要成為“較高優先順序”,需滿足的條件:優先值大於57,600,000,相當於一個比特幣(即1億聰),年齡為一天(144個區塊),交易的大小為250個位元組:

High Priority > 100,000,000 satoshis * 144 blocks / 250 bytes = 57,600,000

區塊中用來儲存交易的前50K位元組是保留給較高優先順序交易的。Jing的節點在填充這50K位元組的時候,會優先考慮這些最高優先順序的交易,不管它們是否包含了礦工費。這種機制使得高優先順序交易即便是零礦工費,也可以優先被處理。

然後,Jing的挖礦節點會選出那些包含最小礦工費的交易,並按照“每千位元組礦工費”進行排序,優先選擇礦工費高的交易來填充剩下的區塊,區塊大小上限為MAX_BLOCK_SIZE

如區塊中仍有剩餘空間,Jing的挖礦節點可以選擇那些不含礦工費的交易。有些礦工會竭盡全力將那些不含礦工費的交易整合到區塊中,而其他礦工也許會選擇忽略這些交易。

在區塊被填滿後,記憶體池中的剩餘交易會成為下一個區塊的候選交易。因為這些交易還留在記憶體池中,所以隨著新的區塊被加到鏈上,這些交易輸入時所引用UTXO的深度(即交易“塊齡”)也會隨著變大。由於交易的優先值取決於它交易輸入的“塊齡”,所以這個交易的優先值也就隨之增長了。最後,一個零礦工費交易的優先值就有可能會滿足高優先順序的門檻,被免費地打包進區塊。

比特幣交易中沒有過期、超時的概念,一筆交易現在有效,那麼它就永遠有效。然而,如果一筆交易只在全網廣播了一次,那麼它只會儲存在一個挖礦節點的記憶體中。因為記憶體池是以未持久化的方式儲存在挖礦節點儲存器中的,所以一旦這個節點重新啟動,記憶體池中的資料就會被完全擦除。而且,即便一筆有效交易被傳播到了全網,如果它長時間未處理,它將從挖礦節點的記憶體池中消失。如果交易本應該在一段時間內被處理而實際沒有,那麼錢包軟體應該重新傳送交易或重新支付更高的礦工費。

現在,Jing的節點從記憶體池中整合到了全部的交易,新的候選區塊包含有418筆交易,總的礦工費為0.09094925個比特幣。你可以通過比特幣核心客戶端命令列來檢視這個區塊,如例8-3所示:

例8-3 區塊277,316

{
    "hash" : "0000000000000001b6b9a13b095e96db41c4a928b97ef2d944a9b31b2cc7bdc4", 
    "confirmations" : 35561,
    "size" : 218629,
    "height" : 277316,
    "version" : 2,
    "merkleroot" :
"c91c008c26e50763e9f548bb8b2fc323735f73577effbc55502c51eb4cc7cf2e", 
    "tx":[
        "d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f",
        "b268b45c59b39d759614757718b9918caf0ba9d97c56f3b91956ff877c503fbe",
        ... 417 more transactions ...
        ],
    "time" : 1388185914,
    "nonce" : 924591752,
    "bits" : "1903a30c",
    "difficulty" : 1180923195.25802612, 
    "chainwork" :
"000000000000000000000000000000000000000000000934695e92aaf53afa1a", 
    "previousblockhash" :
"0000000000000002a7bbd25a417c0374cc55261021e8a9ca74442b01284f0569", 
    "nextblockhash" :
"000000000000000010236c269dd6ed714dd5db39d36b33959079d78dfd431ba7"
}

8.5.2 創幣交易

區塊中的第一筆交易是筆特殊交易,稱為創幣交易或者coinbase交易。這個交易是由Jing的節點構造並用來獎勵礦工們所做的貢獻的。Jing的節點會建立“向Jing的地址支付25.09094928個比特幣”這樣一個交易,把生成交易的獎勵傳送到自己的錢包。Jing挖出區塊獲得的獎勵金額是coinbase獎勵(25個全新的比特幣)和區塊中全部交易礦工費的總和。如例8-4所示:

$ bitcoin-cli getrawtransaction
d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f 1

例8-4 創幣交易

{
    "hex" :
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0f03443b0403858402062f503253482fffffffff0110c08d9500000000232102aa970c592640d19de03ff6f329d6fd2eecb023263b9ba5d1b81c29b523da8b21ac00000000",
    "txid" : "d5ada064c6417ca25c4308bd158c34b77e1c0eca2a73cda16c737e7424afba2f", 
    "version" : 1,
    "locktime" : 0,
    "vin" : [
        {
            "coinbase" : "03443b0403858402062f503253482f",                  "sequence" : 4294967295
        } 
    ],
    "vout" : [ 
        {
            "value" : 25.09094928, 
            "n":0, "
            scriptPubKey" : {
                "asm" : "02aa970c592640d19de03ff6f329d6fd2eecb023263b9ba5d1b81c29b523da8b21OP_CHECKSIG",
                "hex" : "2102aa970c592640d19de03ff6f329d6fd2eecb023263b9ba5d1b81c29b523da8b21ac",
                "reqSigs" : 1, 
                "type" : "pubkey", 
                "addresses" : [
                    "1MxTkeEP2PmHSMze5tUZ1hAV3YTKu2Gh1N"
                ]
            }
        } 
    ],
    "blockhash" : 
"0000000000000001b6b9a13b095e96db41c4a928b97ef2d944a9b31b2cc7bdc4",
    "confirmations" : 35566, 
    "time" : 1388185914, 
    "blocktime" : 1388185914
}

與常規交易不同,創幣交易沒有輸入,不消耗UTXO。它只包含一個被稱作coinbase的輸入,僅僅用來建立新的比特幣。創幣交易有一個輸出,支付到這個礦工的比特幣地址。創幣交易的輸出將這25.09094928個比特幣傳送到礦工的比特幣地址,如本例所示的1MxTkeEP2PmHSMze5tUZ1hAV3YTKu2Gh1N。

8.5.3 Coinbase獎勵與礦工費

為了構造創幣交易,Jing的節點需要計算礦工費的總額,將這418個已新增到區塊交易的輸入和輸出分別進行加總,然後用輸入總額減去輸出總額得到礦工費總額,公式如下:

 Total Fees = Sum(Inputs) - Sum(Outputs)

在區塊277,316中,礦工費的總額是0.09094925個比特幣。

緊接著,Jing的節點計算出這個新區塊正確的獎勵額。獎勵額的計算是基於區塊高度的,以每個區塊50個比特幣為開始,每產生210,000個區塊減半一次。這個區塊高度是277,316,所以正確的獎勵額是25個比特幣。

詳細的計算過程可以參看比特幣核心客戶端中的GetBlockValue函式,如例8-5所示:

例8-5 計算區塊獎勵—Function GetBlockValue, Bitcoin Core Client, main.cpp, line 1305

int64_t GetBlockValue(int nHeight, int64_t nFees) 
{
    int64_t nSubsidy = 50 * COIN;
    int halvings = nHeight / Params().SubsidyHalvingInterval();

    // 如果右移的次數未定義,區塊獎勵強制為零
    if (halvings >= 64)
         return nFees;

    // Subsidy每210,000個區塊減半一次,大概每4年發生一次
    nSubsidy >>= halvings; 

    return nSubsidy + nFees;
}

變數nSubsidy表示初始獎勵額,值為COIN常量(100,000,000聰)與50的乘積,也就是說初始獎勵額為50億聰。

緊接著,這個函式用當前區塊高度除以減半間隔(SubsidyHalvingInterval函式)得到減半次數(變數halvings)。每210,000個區塊為一個減半間隔,對應本例中的區塊277316,所以減半次數為1。

變數halvings最大值64,如果超出這個值,程式碼算得的獎勵額為0,整個函式將只返回礦工費總額,作為獎勵總額。

然後,這個函式會使用二進位制右移操作將獎勵額(變數nSubsidy)右移一位(等同與除以2),每一輪減半右移一次。在這個例子中,對於區塊277,316只需要將值為50億聰的獎勵額右移一次,得到25億聰,也就是25個比特幣的獎勵額。之所以採用二進位制右移操作,是因為相比於整數或浮點數除法,右移操作的效率更高。

最後,將coinbase獎勵額(變數nSubsidy)與礦工費(nFee)總額求和,並返回這個值。

8.5.4 創幣交易的結構

經過計算,Jing的節點構造了一個創幣交易,支付給自己25.09094928枚比特幣。

如例8-4所示,創幣交易的結構比較特殊,與一般交易輸入需要指定一個先前的UTXO不同,它包含一個“coinbase“輸入。在表5-3中,我們已經給出了交易輸入的結構。現在讓我們來比較一下常規交易輸入與創幣交易輸入。表8-1給出了常規交易輸入的結構,表8-2給出的是創幣交易輸入的結構。

表8-1 “普通“交易輸入的結構

長度欄位描述
32 位元組交易雜湊指向包含有將要被花費UTXO的交易
4 位元組交易輸出索引UTXO在交易中的索引,0 從0開始計數
1-9 位元組解鎖指令碼長度解鎖指令碼的長度
(VarInt) 可變長度Unlocking-Script一段指令碼,用來解鎖UTXO鎖定指令碼中的條件
4 bytes順序號當前未啟用的TX替換功能,設定為0xFFFFFFFF

表8-2 生成交易輸入的結構

長度欄位描述
32 位元組交易雜湊不引用任何一個交易,值全部為0
4 位元組交易輸出索引值全部為1
1-9 位元組Coinbase資料長度coinbase資料長度
(VarInt) 可變長度Coinbase資料在v2版本的區塊中,除了需要以區塊高度開始外,其他資料可以任意填寫,用於extra nonce和挖礦標籤
4 bytes順序號值全部為1,0xFFFFFFFF

在創幣交易中,“交易雜湊”欄位32個位元組全部填充0,“交易輸出索引”欄位全部填充0xFF(十進位制的255),這兩個欄位的值表示不引用UTXO。“解鎖指令碼”由coinbase資料代替,資料可以由礦工自定義。

8.5.5 Coinbase資料

創幣交易不包含“解鎖指令碼“(又稱作 scriptSig)欄位,這個欄位被coinbase資料替代,長度最小2位元組,最大100位元組。除了開始的幾個位元組外,礦工可以任意使用coinbase的其他部分,隨意填充任何資料。

以創世塊為例,中本聰在coinbase中填入了這樣的資料“The Times 03/Jan/ 2009 Chancellor on brink of second bailout for banks“(泰晤士報 2009年1月3日 財政大臣將再次對銀行施以援手),表示對日期的證明,同時也表達了對銀行系統的不信任。現在,礦工使用coinbase資料實現extra nonce功能,並嵌入字串來標識挖出它的礦池,這部分內容會在後面的小節描述。coinbase前幾個位元組也曾是可以任意填寫的,不過在後來的第34號比特幣改進提議(BIP34)中規定了版本2的區塊(版本欄位為2的區塊),這個區塊的高度必須跟在指令碼操作“push“之後,填充在coinbase欄位的起始處。

我們以例8-4中的區塊277,316為例,coinbase就是交易輸入的“解鎖指令碼“(或scriptSig)欄位,這個欄位的十六進位制值為03443b0403858402062f503253482f。下面讓我們來解碼這段資料。

第一個位元組是03,指令碼執行引擎執行這個指令將後面3個位元組壓入指令碼棧(見表4-1),緊接著的3個位元組——0x443b04,是以小端格式(最低有效位元組在先)編碼的區塊高度。翻轉位元組序得到0x043b44,表示為十進位制是277,316。

緊接著的幾個十六進位制數(03858402062)用於編碼extra nonce(參見"8.11.1 隨機值升位方案"),或者一個隨機值,從而求解一個適當的工作量證明。

coinbase資料結尾部分(2f503253482f)是ASCII編碼字元 /P2SH/,表示挖出這個區塊的挖礦節點支援BIP0016所定義的pay-to-script-hash(P2SH)改進方案。在P2SH功能引入到比特幣的時候,曾經有過一場對P2SH不同實現方式的投票,候選者是BIP0016和BIP0017。支援BIP0016的礦工將/P2SH/放入coinbase資料中,支援BIP0017的礦工將 p2sh/CHV放入他們的coinbase資料中。最後,BIP0016在選舉中勝出,直到現在依然有很多礦工在他們的coinbase中填入/P2SH/以表示支援這個功能。

例8-6使用了libbitcoin庫(在56頁“其他替代客戶端、資料庫、工具包”中提到)從創世塊中提取coinbase資料,並顯示出中本聰留下的資訊。libbitcoin庫中自帶了一份創世塊的靜態拷貝,所以這段示例程式碼可以直接取自庫中的創世塊資料。

例8-6 從創世區塊中提取coinbase資料

/*
  Display the genesis block message by Satoshi.
*/
#include <iostream>
#include <bitcoin/bitcoin.hpp>

int main() 
{
    // Create genesis block.
    bc::block_type block = bc::genesis_block();
    // Genesis block contains a single coinbase transaction.     assert(block.transactions.size() == 1);
    // Get first transaction in block (coinbase).
    const bc::transaction_type& coinbase_tx = block.transactions[0];
    // Coinbase tx has a single input.
    assert(coinbase_tx.inputs.size() == 1);
    const bc::transaction_input_type& coinbase_input = coinbase_tx.inputs[0]; 
    // Convert the input script to its raw format.
    const bc::data_chunk& raw_message = save_script(coinbase_input.script); 
    // Convert this to an std::string.
    std::string message;
    message.resize(raw_message.size());
    std::copy(raw_message.begin(), raw_message.end(), message.begin());
    // Display the genesis block message.
    std::cout << message << std::endl;
    return 0;
}

在例8-7中,我們使用GNU C++編譯器編譯原始碼並執行得到的可執行檔案

例8-7 編譯並執行satoshi-words示例程式碼

$ # Compile the code
$ g++ -o satoshi-words satoshi-words.cpp $(pkg-config --cflags --libs libbitcoin) $ # Run the executable
$ ./satoshi-words
^D��<GS>^A^DEThe Times 03/Jan/2009 Chancellor on brink of second bailout for banks

8.6 構造區塊頭

為了構造區塊頭,挖礦節點需要填充六個欄位,如表8-3中所示。

表8-3 區塊頭的結構

長度欄位描述
4 位元組版本版本號,用來跟蹤軟體或協議的升級
32 位元組前區塊雜湊鏈中前一個區塊(父區塊)的雜湊值
32 位元組Merkle根一個雜湊值,表示這個區塊中全部交易構成的merkle樹的根
4 位元組時間戳以Unix紀元開始到當下秒數記錄的區塊生成的時刻
4 bytes難度目標該區塊的工作量證明演算法難度目標
4 bytesNonce一個用於工作量證明演算法的計數器

在區塊277,316被挖出的時候,區塊結構中用來表示版本號的欄位值為2,長度為4位元組,以小段格式編碼值為0x20000000。接著,挖礦節點需要填充“前區塊雜湊”,在本例中,這個值為Jing的節點從網路上接收到的區塊277,315的區塊頭雜湊值,它是區塊277316候選區塊的父區塊。區塊277,315的區塊頭雜湊值為:

0000000000000002a7bbd25a417c0374cc55261021e8a9ca74442b01284f0569

為了向區塊頭填充merkle根欄位,要將全部的交易組成一個merkle樹。創幣交易作為區塊中的首個交易,後將餘下的418筆交易添至其後,這樣區塊中的交易一共有419筆。在164頁,我們已經見到過“Merkle樹”,樹中必須有偶數個葉子節點,所以需要複製最後一個交易作為第420個節點,每個節點是對應交易的雜湊值。這些交易的雜湊值逐層地、成對地組合,直到最終組合併成一個根節點。merkle數的根節點將全部交易資料摘要為一個32位元組長度的值,例8-3中merkel根的值如下:

c91c008c26e50763e9f548bb8b2fc323735f73577effbc55502c51eb4cc7cf2e

挖礦節點會繼續新增一個4位元組的時間戳,以Unix紀元時間編碼,即自1970年1月1日0點到當下總共流逝的秒數。本例中的1388185914對應的時間是2013年12月27日,星期五,UTC/GMT。

接下來,節點需要填充難度目標值,為了使得該區塊有效,這個欄位定義了所需滿足的工作量證明的難度。難度在區塊中以“尾數-指數”的格式,編碼並儲存,這種格式稱作“難度位”。這種編碼的首位元組表示指數,後面的3位元組表示尾數(係數)。以區塊277316為例,難度位的值為0x1903a30c,0x19是指數的十六進位制格式,後半部0x03a30c是係數。這部分的概念在第195頁的“難度目標與難度調整”和第194的“難度表示”有詳細的解釋。

最後一個欄位是nonce,初始值為0。

區塊頭完成全部的欄位填充後,挖礦就可以開始進行了。挖礦的目標是找到一個使區塊頭雜湊值小於難度目標的nonce。挖礦節點通常需要嘗試數十億甚至數萬億個不同的nonce取值,直到找到一個滿足條件的nonce值。

8.7 構建區塊

既然Jing的節點已經構建了一個候選區塊,那麼就輪到Jing的礦機對這個新區塊進行“挖掘”,求解工作量證明演算法以使這個區塊有效。從本書中我們已經學習了比特幣系統中不同地方用到的雜湊加密函式。比特幣挖礦過程使用的是SHA256雜湊函式。

用最簡單的術語來說,挖礦就是重複計算區塊頭的雜湊值,不斷修改該引數,直到與雜湊值匹配的一個過程。雜湊函式的結果無法提前得知,也沒有能得到一個特定雜湊值的模式。雜湊函式的這個特性意味著:得到雜湊值的唯一方法是不斷的嘗試,每次隨機修改輸入,直到出現適當的雜湊值。

8.7.1 工作量證明演算法

雜湊函式的輸入資料的長度是任意的,將產生一個長度固定且絕不雷同的值,可將其視為輸入的數字指紋。對於特定輸入,雜湊的結果每次都一樣,任何實現相同雜湊函式的人都可以計算和驗證。一個加密雜湊函式的主要特徵就是不同的輸入幾乎不可能出現相同的數字指紋。因此,相對於隨機選擇輸入,有意地選擇輸入去生成一個想要的雜湊值幾乎是不可能的。

無論輸入的大小是多少,SHA256函式的輸出的長度總是256bit。在例8-8中,我們將使用Python直譯器來計算語句 "I am Satoshi Nakamoto" 的SHA256的雜湊值。

例8-8 SHA256示例

$ python
Python 2.7.1
>>> import hashlib
>>> print hashlib.sha256("I am Satoshi Nakamoto").hexdigest() 5d7c7ba21cbbcd75d14800b100252d5b428e5b1213d27c385bc141ca6b47989e

在例8-8中,5d7c7ba21cbbcd75d14800b100252d5b428e5b1213d27c385bc141ca6b47989e是"I am Satoshi Nakamoto"的雜湊值。改變原句中的任何一個字母、標點、或增加字母都會產生不同的雜湊值。

如果我們改變原句,得到的應該是完全不同的雜湊值。例如,我們在句子末尾加上一個數字,執行例8-9中的Python指令碼。

例8-9 通過迭代 nonce 來生成不同雜湊值的指令碼(SHA256)

# example of iterating a nonce in a hashing algorithm's input
import hashlib
text = "I am Satoshi Nakamoto"
# iterate nonce from 0 to 19
for nonce in xrange(20):
# add the nonce to the end of the text
input = text + str(nonce)
# calculate the SHA-256 hash of the input (text+nonce)
hash = hashlib.sha256(input).hexdigest() # show the input and hash result
print input, '=>', hash

執行這個指令碼就能生成這些只是末尾數字不同的語句的雜湊值。例8-10 中顯示了我們只是增加了這個數字,卻得到了非常不同的雜湊值。

例8-10 通過迭代 nonce 來生成不同雜湊值的指令碼的輸出

$ python hash_example.py

I am Satoshi Nakamoto0 => a80a81401765c8eddee25df36728d732...
I am Satoshi Nakamoto1 => f7bc9a6304a4647bb41241a677b5345f...
I am Satoshi Nakamoto2 => ea758a8134b115298a1583ffb80ae629...
I am Satoshi Nakamoto3 => bfa9779618ff072c903d773de30c99bd...
I am Satoshi Nakamoto4 => bce8564de9a83c18c31944a66bde992f...
I am Satoshi Nakamoto5 => eb362c3cf3479be0a97a20163589038e...
I am Satoshi Nakamoto6 => 4a2fd48e3be420d0d28e202360cfbaba...
I am Satoshi Nakamoto7 => 790b5a1349a5f2b909bf74d0d166b17a...
I am Satoshi Nakamoto8 => 702c45e5b15aa54b625d68dd947f1597...
I am Satoshi Nakamoto9 => 7007cf7dd40f5e933cd89fff5b791ff0...
I am Satoshi Nakamoto10 => c2f38c81992f4614206a21537bd634a...
I am Satoshi Nakamoto11 => 7045da6ed8a914690f087690e1e8d66...
I am Satoshi Nakamoto12 => 60f01db30c1a0d4cbce2b4b22e88b9b...
I am Satoshi Nakamoto13 => 0ebc56d59a34f5082aaef3d66b37a66...
I am Satoshi Nakamoto14 => 27ead1ca85da66981fd9da01a8c6816...
I am Satoshi Nakamoto15 => 394809fb809c5f83ce97ab554a2812c...
I am Satoshi Nakamoto16 => 8fa4992219df33f50834465d3047429...
I am Satoshi Nakamoto17 => dca9b8b4f8d8e1521fa4eaa46f4f0cd...
I am Satoshi Nakamoto18 => 9989a401b2a3a318b01e9ca9a22b0f3...
I am Satoshi Nakamoto19 => cda56022ecb5b67b2bc93a2d764e75f...

每個語句都生成了一個完全不同的雜湊值。它們看起來是完全隨機的,但你在任何計算機上用Python執行上面的指令碼都能重現這些完全相同的雜湊值。

類似這樣在語句末尾的變化的數字叫做nonce。Nonce是用來改變加密函式輸出的,在這個示例中改變了這個語句的SHA256指紋。

為了使這個雜湊演算法變得富有挑戰,我們來設定一個具有任意性的目標:找到一個語句,使之雜湊值的十六進位制表示以0開頭。幸運的是,這很容易!在例8-10中語句 "I am Satoshi Nakamoto13" 的雜湊值是 0ebc56d59a34f5082aaef3d66b37a661696c2b618e62432727216ba9531041a5,剛好滿足條件。我們得到它用了13次。用概率的角度來看,如果雜湊函式的輸出是平均分佈的,我們可以期望每16次得到一個以0開頭的雜湊值(十六進位制每一位數字為0到F)。從數字的角度來看,我們要找的是小於 0x1000000000000000000000000000000000000000000000000000000000000000的雜湊值。我們稱這個為目標閥值,我們的目的是找到一個小於這個目標的雜湊值。如果我們減小這個目標值,那找到一個小於它的雜湊值會越來越難。

簡單打個比方,想象人們不斷扔一對色子以得到小於一個特定點數的遊戲。第一局,目標是12。只要你不扔出兩個6,你就會贏。然後下一局目標為11。玩家只能扔10或更小的點數才能贏,不過也很簡單。假如幾局之後目標降低為了5。現在有一半機率以上扔出來的色子加起來點數會超過5,因此無效。隨著目標越來越小,要想贏的話,扔色子的次數會指數級的上升。最終當目標為2時(最小可能點數),只有一個人平均扔36次或2%扔的次數中,他才能贏。

在例8-10中,成功的nonce為13,且這個結果能被所有人獨立確認。任何人將13加到語句 "I am Satoshi Nakamoto" 後面再計算雜湊值都能確認它比目標值要小。這個正確的結果同時也是工作量證明(Proof of Work),因為它證明我們的確花時間找到了這個nonce。驗證這個雜湊值只需要一次計算,而我們找到它卻花了13次。如果目標值更小(難度更大),那我們需要多得多的雜湊計算才能找到合適的nonce,但其他人驗證它時只需要一次雜湊計算。此外,知道目標值後,任何人都可以用統計學來估算其難度,因此就能知道找到這個nonce需要多少工作。

比特幣的工作量證明和例8-10中的挑戰非常類似。礦工用一些交易構建一個候選區塊。接下來,這個礦工計算這個區塊頭資訊的雜湊值,看其是否小於當前目標值。如果這個雜湊值不小於目標值,礦工就會修改這個nonce(通常將之加1)然後再試一次。按當前比特幣系統的難度,礦工得試10^15次(10的15次方)才能找到一個合適的nonce使區塊頭資訊雜湊值足夠小。

例8-11是一個簡化很多的工作量證明演算法的實現。

例8-11 簡化的工作量證明演算法

#!/usr/bin/env python
# example of proof-of-work algorithm

import hashlib
import time

max_nonce = 2 ** 32 # 4 billion

def proof_of_work(header, difficulty_bits):

    # calculate the difficulty target
    target = 2 ** (256-difficulty_bits) 

    for nonce in xrange(max_nonce):
        hash_result = hashlib.sha256(str(header)+str(nonce)).hexdigest() 

        # check if this is a valid result, below the target

        if long(hash_result, 16) < target:
            print "Success with nonce %d" % nonce 
            print "Hash is %s" % hash_result 
            return (hash_result,nonce)

    print "Failed after %d (max_nonce) tries" % nonce 
    return nonce

if __name__ == '__main__': 

    nonce = 0
    hash_result = ''

    # difficulty from 0 to 31 bits
    for difficulty_bits in xrange(32): 

        difficulty = 2 ** difficulty_bits
        print "Difficulty: %ld (%d bits)" % (difficulty, difficulty_bits) 

        print "Starting search..."

        # checkpoint the current time
        start_time = time.time()

        # make a new block which includes the hash from the previous block 
        # we fake a block of transactions - just a string
        new_block = 'test block with transactions' + hash_result

        # find a valid nonce for the new block
        (hash_result, nonce) = proof_of_work(new_block, difficulty_bits) 

        # checkpoint how long it took to find a result
        end_time = time.time()

        elapsed_time = end_time - start_time
        print "Elapsed Time: %.4f seconds" % elapsed_time 

        if elapsed_time > 0:

            # estimate the hashes per second
            hash_power = float(long(nonce)/elapsed_time)
            print "Hashing Power: %ld hashes per second" % hash_power

你可以任意調整難度值(按二進位制bit數來設定,即雜湊值開頭多少個bit必須是0)。然後執行程式碼,看看在你的計算機上求解需要多久。在例8-12中,你可以看到該程式在一個普通膝上型電腦上的執行情況。

例8-12 多種難度值的工作量證明演算法的執行輸出

$ python proof-of-work-example.py*

Difficulty: 1 (0 bits)

[...]

Difficulty: 8 (3 bits)
Starting search...
Success with nonce 9
Hash is 1c1c105e65b47142f028a8f93ddf3dabb9260491bc64474738133ce5256cb3c1
Elapsed Time: 0.0004 seconds
Hashing Power: 25065 hashes per second
Difficulty: 16 (4 bits)
Starting search...
Success with nonce 25
Hash is 0f7becfd3bcd1a82e06663c97176add89e7cae0268de46f94e7e11bc3863e148
Elapsed Time: 0.0005 seconds
Hashing Power: 52507 hashes per second
Difficulty: 32 (5 bits)
Starting search...
Success with nonce 36
Hash is 029ae6e5004302a120630adcbb808452346ab1cf0b94c5189ba8bac1d47e7903
Elapsed Time: 0.0006 seconds
Hashing Power: 58164 hashes per second

[...]

Difficulty: 4194304 (22 bits)
Starting search...
Success with nonce 1759164
Hash is 0000008bb8f0e731f0496b8e530da984e85fb3cd2bd81882fe8ba3610b6cefc3
Elapsed Time: 13.3201 seconds
Hashing Power: 132068 hashes per second
Difficulty: 8388608 (23 bits)
Starting search...
Success with nonce 14214729
Hash is 000001408cf12dbd20fcba6372a223e098d58786c6ff93488a9f74f5df4df0a3
Elapsed Time: 110.1507 seconds
Hashing Power: 129048 hashes per second
Difficulty: 16777216 (24 bits)
Starting search...
Success with nonce 24586379
Hash is 0000002c3d6b370fccd699708d1b7cb4a94388595171366b944d68b2acce8b95
Elapsed Time: 195.2991 seconds
Hashing Power: 125890 hashes per second

[...]

Difficulty: 67108864 (26 bits)
Starting search...
Success with nonce 84561291
Hash is 0000001f0ea21e676b6dde5ad429b9d131a9f2b000802ab2f169cbca22b1e21a
Elapsed Time: 665.0949 seconds
Hashing Power: 127141 hashes per second

你可以看出,隨著難度位一位一位地增加,查詢正確結果的時間會呈指數級增長。如果你考慮整個256bit數字空間,每次要求多一個0,你就把雜湊查詢空間縮減了一半。在例8-12中,為尋找一個nonce使得雜湊值開頭的26位值為0,一共嘗試了8千多萬次。即使家用筆記本每秒可以達270,000多次雜湊計算,這個查詢依然需要6分鐘。

在寫這本書的時候,比特幣網路要尋找區塊頭資訊雜湊值小於 000000000000004c296e6376db3a241271f43fd3f5de7ba18986e517a243baa7。可以看出,這個目標雜湊值開頭的0多了很多。這意味著可接受的雜湊值範圍大幅縮減,因而找到正確的雜湊值更加困難。生成下一個區塊需要網路每秒計算1.5 x 1017次雜湊。這看起來像是不可能的任務,但幸運的是比特幣網路已經擁有100PH每秒(petahashes per second, peta-為 1015)的處理能力,平均每10分鐘就可以找到一個新區塊。

8.7.2 難度表示

在例8-3中,我們在區塊中看到難度目標,其被標為"難度位"或簡稱"bits"。在區塊277,316中,它的值為 0x1903a30c。這個標記的值被存為係數/指數格式,前兩位十六進位制數字為冪,接下來得六位為係數。在這個區塊裡,0x19為冪,而0x03a30c為係數。

計算難度目標的公式為:

 target = coefficient * 2^(8 * (exponent – 3))

由此公式及難度位的值 0x1903a30c,可得:

target = 0x03a30c * 2^(0x08 * (0x19 - 0x03))

=> target = 0x03a30c * 2^(0x08 * 0x16)

=> target = 0x03a30c * 2^0xB0

按十進位制計算為:

=> target = 238,348 * 2^176
=> target =
22,829,202,948,393,929,850,749,706,076,701,368,331,072,452,018,388,575,715,328

轉化為十六進位制後為:

=> target =0x0000000000000003A30C00000000000000000000000000000000000000000000

也就是說高度為277,316的有效區塊的頭資訊雜湊值是小於這個目標值的。這個數字的二進位制表示中前60位都是0。在這個難度上,一個每秒可以處理1萬億個雜湊計算的礦工(1 tera-hash per second 或 1 TH/sec)平均每8,496個區塊才能找到一個正確結果,換句話說,平均每59天,才能為某一個區塊找到正確的雜湊值。

8.7.3 難度目標與難度調整

如前所述,目標決定了難度,進而影響求解工作量證明演算法所需要的時間。那麼問題來了:為什麼這個難度值是可調整的?由誰來調整?如何調整?

比特幣的區塊平均每10分鐘生成一個。這就是比特幣的心跳,是貨幣發行速率和交易達成速度的基礎。不僅是在短期內,而是在幾十年內它都必須要保持恆定。在此期間,計算機效能將飛速提升。此外,參與挖礦的人和計算機也會不斷變化。為了能讓新區塊的保持10分鐘一個的產生速率,挖礦的難度必須根據這些變化進行調整。事實上,難度是一個動態的引數,會定期調整以達到每10分鐘一個新區塊的目標。簡單地說,難度被設定在,無論挖礦能力如何,新區塊產生速率都保持在10分鐘一個。

那麼,在一個完全去中心化的網路中,這樣的調整是如何做到的呢?難度的調整是在每個完整節點中獨立自動發生的。每2,016個區塊中的所有節點都會調整難度。難度的調整公式是由最新2,016個區塊的花費時長與20,160分鐘(兩週,即這些區塊以10分鐘一個速率所期望花費的時長)比較得出的。難度是根據實際時長與期望時長的比值進行相應調整的(或變難或變易)。簡單來說,如果網路發現區塊產生速率比10分鐘要快時會增加難度。如果發現比10分鐘慢時則降低難度。

這個公式可以總結為如下形式:

New Difficulty = Old Difficulty * (Actual Time of Last 2016 Blocks / 20160 minutes)

例8-13展示了比特幣核心客戶端中的難度調整程式碼。

例8-13 工作量證明的難度調整 原始檔 pow.cpp 第43行函式 GetNextWorkRequired()

// Go back by what we want to be 14 days worth of blocks
const CBlockIndex* pindexFirst = pindexLast;
for (int i = 0; pindexFirst && i < Params().Interval()-1; i++)
    pindexFirst = pindexFirst->pprev;
assert(pindexFirst);
// Limit adjustment step
int64_t nActualTimespan = pindexLast->GetBlockTime() - pindexFirst->GetBlockTime(); LogPrintf(" nActualTimespan = %d before bounds\n", nActualTimespan);
if (nActualTimespan < Params().TargetTimespan()/4)
    nActualTimespan = Params().TargetTimespan()/4; 
if (nActualTimespan > Params().TargetTimespan()*4) 
    nActualTimespan = Params().TargetTimespan()*4;

// Retarget
uint256 bnNew;
uint256 bnOld;
bnNew.SetCompact(pindexLast->nBits);
bnOld = bnNew;
bnNew *= nActualTimespan;
bnNew /= Params().TargetTimespan();

if (bnNew > Params().ProofOfWorkLimit()) 
    bnNew = Params().ProofOfWorkLimit();

引數Interval(2,016區塊)和TargetTimespan(1,209,600秒即兩週) 的定義在檔案chainparams.cpp中。

為了防止難度的變化過快,每個週期的調整幅度必須小於一個因子(值為4)。如果要調整的幅度大於4倍,則按4倍調整。由於在下一個2,016區塊的週期不平衡的情況會繼續存在,所以進一步的難度調整會在下一週期進行。因此平衡雜湊計算能力和難度的巨大差異有可能需要花費幾個2,016區塊週期才會完成。

尋找一個比特幣區塊需要整個網路花費10分鐘來處理,每發現2,016個區塊時會根據前2,016個區塊完成的時間對難度進行調整。

值得注意的是目標難度與交易的數量和金額無關。這意味著雜湊算力的強弱,即讓比特幣更安全的電力投入量,與交易的數量完全無關。換句話說,當比特幣的規模變得更大,使用它的人數更多時,即使雜湊算力保持當前的水平,比特幣的安全性也不會受到影響。雜湊算力的增加表明更多的人為得到比特幣回報而加入了挖礦隊伍。只要為了回報,公平正當地從事挖礦的礦工群體保持足夠的雜湊算力,"接管"攻擊就不會得逞,讓比特幣的安全無虞。

目標難度和挖礦電力消耗與將比特幣兌換成現金以支付這些電力之間的關係密切相關。高效能挖礦系統就是要用當前矽晶片以最高效的方式將電力轉化為雜湊算力。挖礦市場的關鍵因素就是每度電轉換為比特幣後的價格。因為這決定著挖礦活動的營利性,也因此刺激著人們選擇進入或退出挖礦市場。

8.8 成功構建區塊

前面已經看到,Jing的節點建立了一個候選區塊,準備拿它來挖礦。Jing有幾個安裝了ASIC(專用積體電路)的礦機,上面有成千上萬個積體電路可以超高速地並行執行SHA256演算法。這些定製的硬體通過USB連線到他的挖礦節點上。接下來,執行在Jing的桌面電腦上的挖礦節點將區塊頭資訊傳送給這些硬體,讓它們以每秒億萬次的速度進行nonce測試。

在對區塊277,316的挖礦工作開始大概11分鐘後,這些硬體裡的其中一個求得了解併發回挖礦節點。當把這個結果放進區塊頭時,nonce 4,215,469,401 就會產生一個區塊雜湊值:

0000000000000002a7bbd25a417c0374cc55261021e8a9ca74442b01284f0569

而這個值小於難度目標值:

0000000000000003A30C00000000000000000000000000000000000000000000

Jing的挖礦節點立刻將這個區塊發給它的所有相鄰節點。這些節點在接收並驗證這個新區塊後,也會繼續傳播此區塊。當這個新區塊在網路中擴散時,每個節點都會將它作為區塊277,316加到自身節點的區塊鏈副本中。當挖礦節點收到並驗證了這個新區塊後,它們會放棄之前對構建這個相同高度區塊的計算,並立即開始計算區塊鏈中下一個區塊的工作。

下節將介紹節點進行區塊驗證、最長鏈選擇、達成共識,並以此形成一個去中心化區塊鏈的過程。

8.9 校驗新區塊

比特幣共識機制的第三步是通過網路中的每個節點獨立校驗每個新區塊。當新區塊在網路中傳播時,每一個節點在將它轉發到其節點之前,會進行一系列的測試去驗證它。這確保了只有有效的區塊會在網路中傳播。獨立校驗還確保了誠實的礦工生成的區塊可以被納入到區塊鏈中,從而獲得獎勵。行為不誠實的礦工所產生的區塊將被拒絕,這不但使他們失去了獎勵,而且也浪費了本來可以去尋找工作量證明解的機會,因而導致其電費虧損。

當一個節點接收到一個新的區塊,它將對照一個長長的標準清單對該區塊進行驗證,若沒有通過驗證,這個區塊將被拒絕。這些標準可以在比特幣核心客戶端的CheckBlock函式和CheckBlockHead函式中獲得,它包括:

▷ 區塊的資料結構語法上有效 
▷ 區塊頭的雜湊值小於目標難度(確認包含足夠的工作量證明) 
▷ 區塊時間戳早於驗證時刻未來兩個小時(允許時間錯誤) 
▷ 區塊大小在長度限制之內 
▷ 第一個交易(且只有第一個)是coinbase交易 
▷ 使用檢查清單驗證區塊內的交易並確保它們的有效性,本書177頁 
▷ “交易的獨立校驗”一節已經討論過這個清單。

每一個節點對每一個新區塊的獨立校驗,確保了礦工無法欺詐。在前面的章節中,我們看到了礦工們如何去記錄一筆交易,以獲得在此區塊中創造的新比特幣和交易費。為什麼礦工不為他們自己記錄一筆交易去獲得數以千計的比特幣?這是因為每一個節點根據相同的規則對區塊進行校驗。一個無效的coinbase交易將使整個區塊無效,這將導致該區塊被拒絕,因此,該交易就不會成為總賬的一部分。礦工們必須構建一個完美的區塊,基於所有節點共享的規則,並且根據正確工作量證明的解決方案進行挖礦,他們要花費大量的電力挖礦才能做到這一點。如果他們作弊,所有的電力和努力都會浪費。這就是為什麼獨立校驗是去中心化共識的重要組成部分。

8.10 區塊鏈的組裝與選擇

比特幣去中心化的共識機制的最後一步是將區塊集合至有最大工作量證明的鏈中。一旦一個節點驗證了一個新的區塊,它將嘗試將新的區塊連線到到現存的區塊鏈,將它們組裝起來。

節點維護三種區塊:第一種是連線到主鏈上的,第二種是從主鏈上產生分支的(備用鏈),最後一種是在已知鏈中沒有找到已知父區塊的。在驗證過程中,一旦發現有不符合標準的地方,驗證就會失敗,這樣區塊會被節點拒絕,所以也不會加入到任何一條鏈中。

任何時候,主鏈都是累計了最多難度的區塊鏈。在一般情況下,主鏈也是包含最多區塊的那個鏈,除非有兩個等長的鏈並且其中一個有更多的工作量證明。主鏈也會有一些分支,這些分支中的區塊與主鏈上的區塊互為“兄弟”區塊。這些區塊是有效的,但不是主鏈的一部分。 保留這些分支的目的是如果在未來的某個時刻它們中的一個延長了並在難度值上超過了主鏈,那麼後續的區塊就會引用它們。在“8.10.1 區塊鏈分叉”,我們將會看到在同樣的區塊高度,幾乎同時挖出區塊時,候選鏈是如何產生的。

當節點接收到新區塊,它會嘗試將這個區塊插入到現有區塊鏈中。節點會看一下這個區塊的“previous block hash”欄位,這個欄位是該區塊對其父區塊的引用。同時,新的節點將嘗試在已存在的區塊鏈中找出這個父區塊。大多數情況下,父區塊是主塊鏈的“頂點”,這就意味著這個新的區塊延長了主鏈。舉個例子,一個新的區塊——區塊277,316引用了它的父區塊——區塊277,315。大部分收到了區塊277,316的節點將區塊277,315作為主鏈的頂點,連線這個新區塊並延長區塊鏈。

有時候,新區塊所延長的區塊鏈並不是主鏈,這一點我們將在“8.10.1 區塊鏈分叉”中看到。在這種情況下,節點將新的區塊新增到備用鏈,同時比較備用鏈與主鏈的難度。如果備用鏈比主鏈積累了更多的難度,節點將收斂於備用鏈,意味著節點將選擇備用鏈作為其新的主鏈,而之前那個老的主鏈則成為了備用鏈。如果節點是一個礦工,它將開始構造新的區塊,來延長這個更新更長的區塊鏈。

如果節點收到了一個有效的區塊,而在現有的區塊鏈中卻未找到它的父區塊,那麼這個區塊被認為是“孤塊”。孤塊會被儲存在孤塊池中,直到它們的父區塊被節點收到。一旦收到了父區塊並且將其連線到現有區塊鏈上,節點就會將孤塊從孤塊池中取出,並且連線到它的父區塊,讓它作為區塊鏈的一部分。當兩個區塊在很短的時間間隔內被挖出來,節點有可能會以相反的順序接收到它們,這個時候孤塊現象就會出現。

選擇了最大難度的區塊鏈後,所有的節點最終在全網範圍內達成共識。隨著更多的工作量證明被新增到鏈中,鏈的暫時性差異最終會得到解決。挖礦節點通過“投票”來選擇它們想要延長的區塊鏈,當它們挖出一個新塊並且延長了一個鏈,新塊本身就代表它們的投票。

相互競爭的鏈之間是存在差異的,下節我們將看到節點是怎樣通過獨立選擇最長難度鏈來解決這種差異的。

8.10.1 區塊鏈分叉

因為區塊鏈是去中心化的資料結構,所以不同副本之間不能總是保持一致。區塊有可能在不同時間到達不同節點,導致節點有不同的區塊鏈視角。解決的辦法是,每一個節點總是選擇並嘗試延長代表累計了最大工作量證明的區塊鏈,也就是最長的或最大累計難度的鏈。節點通過將記錄在每個區塊中的難度加總起來,得到建立這個鏈所要付出的工作量證明的總量。只要所有的節點選擇最長累計難度的區塊鏈,整個比特幣網路最終會收斂到一致的狀態。分叉即在不同區塊鏈間發生的臨時差異,當更多的區塊新增到了某個分叉中,這個問題便會迎刃而解。

在下面的圖例中,我們可以瞭解網路中發生分叉的過程。圖例代表簡單的全球比特幣網路,在真實的情況下,比特幣網路的拓撲結構不是基於地理位置組織起來的。相反,在同一個網路中相互連線的節點,可能在地理位置上相距遙遠,我們採用基於地理的拓撲是為了更加簡潔地描述分叉。在真實比特幣網路裡,節點間的距離按“跳”而不是按照真實位置來衡量。為了便於描述,不同的區塊被標示為不同的顏色,傳播這些區塊的節點網路也被標上顏色。

在第一張圖(圖8-2)中,網路有一個統一的區塊鏈視角,以藍色區塊為主鏈的“頂點”。


圖8-2 形象化的區塊鏈分叉事件——分叉之前

當有兩個候選區塊同時想要延長最長區塊鏈時,分叉事件就會發生。正常情況下,分叉發生在兩名礦工在較短的時間內,各自都算得了工作量證明解的時候。兩個礦工在各自的候選區塊一發現解,便立即傳播自己的“獲勝”區塊到網路中,先是傳播給鄰近的節點而後傳播到整個網路。每個收到有效區塊的節點都會將其併入並延長區塊鏈。如果該節點在隨後又收到了另一個候選區塊,而這個區塊又擁有同樣父區塊,那麼節點會將這個區塊連線到候選鏈上。其結果是,一些節點收到了一個候選區塊,而另一些節點收到了另一個候選區塊,這時兩個不同版本的區塊鏈就出現了。

在圖8-3中,我們看到兩個礦工幾乎同時挖到了兩個不同的區塊。這兩個區塊是頂點區塊——藍色區塊的子區塊,可以延長這個區塊鏈。為了便於跟蹤這個分叉事件,我們設定有一個被標記為紅色的、來自加拿大的區塊,還有一個被標記為綠色的、來自澳大利亞的區塊。


圖8-3 形象化的區塊鏈分叉事件:同時發現兩個區塊

假設有這樣一種情況,一個在加拿大的礦工發現了“紅色”區塊的工作量證明解,在“藍色”的父區塊上延長了塊鏈。幾乎同一時刻,一個澳大利亞的礦工找到了“綠色”區塊的解,也延長了“藍色”區塊。那麼現在我們就有了兩個區塊:一個是源於加拿大的“紅色”區塊;另一個是源於澳大利亞的“綠色”。這兩個區塊都是有效的,均包含有效的工作量證明解並延長同一個父區塊。這個兩個區塊可能包含了幾乎相同的交易,只是在交易的排序上有些許不同。

當這個兩個區塊傳播時,一些節點首先收到“紅色”區塊,一些節點收到“綠色”區塊。如圖8-4所示,比特幣網路上的節點對於區塊鏈的頂點產生了分歧,一派以紅色區塊為頂點,而另一派以綠色區塊為頂點。


圖8-4 形象化的區塊鏈分叉事件:兩個區塊的傳播將網路分裂了

從那時起,比特幣網路中鄰近(網路拓撲上的鄰近,而非地理上的)加拿大的節點會首先收到“紅色”區塊,並建立一個最大累計難度的區塊,“紅色”區塊為這個鏈的最後一個區塊(藍色-紅色),同時忽略晚一些到達的“綠色”區塊。相比之下,離澳大利亞更近的節點會判定“綠色”區塊勝出,並以它為最後一個區塊來延長區塊鏈(藍色-綠色),忽略晚幾秒到達的“紅色”區塊。那些首先收到“紅色”區塊的節點,會即刻以這個區塊為父區塊來產生新的候選區塊,並嘗試尋找這個候選區塊的工作量證明解。同樣地,接受“綠色”區塊的節點會以這個區塊為鏈的頂點開始生成新塊,延長這個鏈。

分叉問題幾乎總是在一個區塊內就被解決了。網路中的一部分算力專注於“紅色”區塊為父區塊,在其之上建立新的區塊;另一部分算力則專注在“綠色”區塊上。即便算力在這兩個陣營中平均分配,也總有一個陣營搶在另一個陣營前發現工作量證明解並將其傳播出去。在這個例子中我們可以打個比方,假如工作在“綠色”區塊上的礦工找到了一個“粉色”區塊延長了區塊鏈(藍色-綠色-粉色),他們會立刻傳播這個新區塊,整個網路會都會認為這個區塊是有效的,如圖8-5所示。


圖8-5 形象化的區塊鏈分叉事件:新區塊延長了分支

所有在上一輪選擇“綠色”區塊為勝出者的節點會直接將這條鏈延長一個區塊。然而,那些選擇“紅色”區塊為勝出者的節點現在會看到兩個鏈:“藍色-綠色-粉色”和“藍色-紅色”。如圖8-6所示,這些節點會根據結果將“藍色-綠色-粉色”這條鏈設定為主鏈,將“藍色-紅色”這條鏈設定為備用鏈。這些節點接納了新的更長的鏈,被迫改變了原有對區塊鏈的觀點,這就叫做鏈的重新共識。因為“紅”區塊做為父區塊已經不在最長鏈上,導致了他們的候選區塊已經成為了“孤塊”,所以現在任何原本想要在“藍色-紅色”鏈上延長區塊鏈的礦工都會停下來。全網將“藍色-綠色-粉色”這條鏈識別為主鏈,“粉色”區塊為這條鏈的最後一個區塊。全部礦工立刻將他們產生的候選區塊的父區塊切換為“粉色”,來延長“藍色-綠色-粉色”這條鏈。


圖8-6 形象化的區塊鏈分叉事件:全網在最長鏈上重新共識

從理論上來說,兩個區塊的分叉是有可能的,這種情況發生在因先前分叉而相互對立起來的礦工,又幾乎同時發現了兩個不同區塊的解。然而,這種情況發生的機率是很低的。單區塊分叉每週都會發生,而雙塊分叉則非常罕見。

比特幣將區塊間隔設計為10分鐘,是在更快速的交易確認和更低的分叉概率間作出的妥協。更短的區塊產生間隔會讓交易清算更快地完成,也會導致更加頻繁地區塊鏈分叉。與之相對地,更長的間隔會減少分叉數量,卻會導致更長的清算時間。

8.11 挖礦和算力競賽

比特幣挖礦是一個極富競爭性的行業。自從比特幣存在開始,每年比特幣算力都成指數增長。一些年份的增長還體現出技術的變革,比如在2010年和2011年,很多礦工開始從使用CPU升級到使用GPU,進而使用FGPA(現場可程式設計門陣列)挖礦。在2013年,ASIC挖礦的引入,把SHA256演算法直接固化在挖礦專用的矽晶片上,引起了算力的另一次巨大飛躍。一臺採用這種晶片的礦機可以提供的算力,比2010年比特幣網路的整體算力還要大。

下表表示了比特幣網路開始執行後最初五年的總算力:

2009 
0.5 MH/秒–8 MH/秒 (16倍增長)

2010 
8 MH/秒–116 GH/秒 (14,500倍增長)

2011 
16 GH/秒–9 TH/秒 (562倍增長)

2012 
9 TH/秒–23 TH/秒 (2.5倍增長)

2013 
23 TH/秒–10 PH/秒 (450倍增長)

2014 
10 PH/秒–150 PH/秒 到8月為止 (15倍增長)

在圖8-7的圖表中,我們可以看到近兩年裡,礦業和比特幣的成長引起了比特幣網路算力的指數增長(每秒網路總算力)。


圖8-7 近兩年的總算力,G次hash/秒

隨著比特幣挖礦算力的爆炸性增長,與之匹配的難度也相應增長。圖8-8中的相對難度值顯示了當前難度與最小難度(第一個塊的難度)的比例。


圖8-8 近兩年的比特幣難度值

近兩年,ASIC晶片變得更加密集,特徵尺寸接近晶片製造業前沿的22奈米。挖礦的利潤率驅動這個行業以比通用計算更快的速度發展。目前,ASIC製造商的目標是超越通用CPU晶片製造商,設計特徵尺寸為16奈米的晶片。對比特幣挖礦而言,已經沒有更多飛躍的空間,因為這個行業已經觸及了摩爾定律的最前沿。摩爾定律指出計算能力每18個月增加一倍。儘管如此,隨著更高密度的晶片和資料中心的部署競賽,網路算力繼續保持同步的指數增長。現在的競爭已經不再是比較單一晶片的能力,而是一個礦場能塞進多少晶片,並處理好散熱和供電問題。

8.11.1 隨機值升位方案

2012年以來,比特幣挖礦發展出一個解決區塊頭基本結構限制的方案。在比特幣的早期,礦工可以通過遍歷隨機數(Nonce)獲得符合要求的hash來挖出一個塊。難度增長後,礦工經常在嘗試了40億個值後仍然沒有出塊。然而,這很容易通過讀取塊的時間戳並計算經過的時間來解決。因為時間戳是區塊頭的一部分,它的變化可以讓礦工用不同的隨機值再次遍歷。當挖礦硬體的速度達到了4GH/秒,這種方法變得越來越困難,因為隨機數的取值在一秒內就被用盡了。當出現ASIC礦機並很快達到了TH/秒的hash速率後,挖礦軟體為了找到有效的塊,需要更多的空間來儲存nonce值。可以把時間戳延後一點,但將來如果把它移動得太遠,會導致區塊變為無效。區塊頭需要一個新的“差異性”的資訊來源。解決方案是使用coinbase交易作為額外的隨機值來源,因為coinbase指令碼可以儲存2-100位元組的資料,礦工們開始使用這個空間作為額外隨機值的來源,允許他們去探索一個大得多的區塊頭值範圍來找到有效的塊。這個coinbase交易包含在merkle樹中,這意味著任何coinbase指令碼的變化將導致Merkle根的變化。8個位元組的額外隨機數,加上4個位元組的“標準”隨機數,允許礦工每秒嘗試296(8後面跟28個零)種可能性而無需修改時間戳。如果未來礦工可以嘗試所有的可能性,他們還可以通過修改時間戳來解決。同樣,coinbase指令碼中也有更多額外的空間可以為將來隨機數的擴充套件做準備。

8.11.2 礦池

在這個激烈競爭的環境中,個體礦工獨立工作(也就是solo挖礦)沒有一點機會。他們找到一個區塊以抵消電力和硬體成本的可能性非常小,以至於可以稱得上是賭博,就像是買彩票。就算是最快的消費型ASIC也不能和那些在巨大機房裡擁有數萬晶片並靠近水電站的商業礦場競爭。現在礦工們合作組成礦池,彙集數以千計參與者們的算力並分享獎勵。通過參加礦池,礦工們得到整體回報的一小部分,但通常每天都能得到,因而減少了不確定性。

讓我們來看一個具體的例子。假設一名礦工已經購買了算力共計6,000GH/S,或6TH/S的裝置,在2014年8月,它的價值大約是1萬美元。該裝置執行功率為3千瓦(KW),每日耗電72度,每日平均成本7或8美元。以目前的比特幣難度,該礦工平均每155天或5個月可能solo出一個塊。如果這個礦工確實在這個時限內挖出一個區塊,獎勵25比特幣,如果每個比特幣價格約為600美元,可以得到15,000美元的收入。這可以覆蓋整個時間週期內的裝置和電力成本,還剩下大約3,000美元的淨利潤。然而,在5個月的時間週期內能否挖出一個塊主要靠礦工的運氣。他有可能在五個月中得到兩個塊從而賺到非常大的利潤。或者,他可能10個月都找不到一個塊,從而遭受經濟損失。更糟的是,比特幣的工作證明(POW)演算法的難度可能在這段時間內顯著上升,按照目前算力增長的速度,這意味著礦工在裝置被下一代更有效率的礦機取代之前,最多有6個月的時間取得成果。如果這個礦工加入礦池,而不是等待5個月內可能出現一次的暴利,他每週能賺取大約500-700美元。礦池的常規收入能幫他隨時間攤銷硬體和電力的成本,並且不用承擔巨大的風險。在7到9個月後,硬體仍然會過時,風險仍然很高,但在此期間的收入至少是定期的和可靠的。

礦池通過專用挖礦協議協調成百上千的礦工。個人礦工在建立礦池賬號後,設定他們的礦機連線到礦池伺服器。他們的挖礦裝置在挖礦時保持和礦池伺服器的連線,和其他礦工同步各自的工作。這樣,礦池中的礦工分享挖礦任務,之後分享獎勵。

成功出塊的獎勵支付到礦池的比特幣地址,而不是單個礦工的。一旦獎勵達到一個特定的閾值,礦池伺服器便會定期支付獎勵到礦工的比特幣地址。通常情況下,礦池伺服器會為提供礦池服務收取一個百分比的費用。

參加礦池的礦工把搜尋候選區塊的工作量分割,並根據他們挖礦的貢獻賺取“份額”。礦池為賺取“份額”設定了一個低難度的目標,通常比比特幣網路難度低1000倍以上。當礦池中有人成功挖出一塊,礦池獲得獎勵,並和所有礦工按照他們做出貢獻的“份額”數的比例分配。

礦池對任何礦工開放,無論大小、專業或業餘。一個礦池的參與者中,有人只有一臺小礦機,而有些人有一車庫高階挖礦硬體。有人只用幾十度電挖礦,也有人會用一個資料中心消耗兆瓦級的電量。礦池如何衡量每個人的貢獻,既能公平分配獎勵,又避免作弊的可能?答案是在設定一個較低難度的前提下,使用比特幣的工作量證明演算法來衡量每個礦工的貢獻。因此,即使是池中最小的礦工也經常能分得獎勵,這足以激勵他們為礦池做出貢獻。通過設定一個較低的取得份額的難度,礦池可以計量出每個礦工完成的工作量。每當礦工發現一個小於礦池難度的區塊頭hash,就證明了它已經完成了尋找結果所需的hash計算。更重要的是,這些為取得份額貢獻而做的工作,能以一個統計學上可衡量的方法,整體尋找一個比特幣網路的目標雜湊值。成千上萬的礦工嘗試較小區間的hash值,最終可以找到符合比特幣網路要求的結果。

讓我們回到骰子游戲的比喻。如果骰子玩家的目標是扔骰子結果都小於4(整體網路難度),一個礦池可以設定一個更容易的目標,統計有多少次池中的玩家扔出的結果小於8。當池中的玩家扔出的結果小於8(礦池份額目標),他們得到份額,但他們沒有贏得遊戲,因為沒有完成遊戲目標(小於4)。但池中的玩家會更經常的達到較容易的礦池份額目標,規律地賺取他們的份額,儘管他們沒有完成更難的贏得比賽的目標。

時不時地,池中的一個成員有可能會扔出一個小於4的結果,礦池獲勝。然後,收益可以在池中玩家獲得的份額基礎上分配。儘管目標設定為8或更少並沒有贏得遊戲,但是這是一個衡量玩家們扔出的點數的公平方法,同時它偶爾會產生一個小於4的結果。

同樣的,一個礦池會將礦池難度設定在保證一個單獨的礦工能夠頻繁地找到一個符合礦池難度的區塊頭hash來贏取份額。時不時的,某次嘗試會產生一個符合比特幣網路目標的區塊頭hash,產生一個有效塊,然後整個礦池獲勝。

8.11.2.1 託管礦池

大部分礦池是“託管的”,意思是有一個公司或者個人經營一個礦池伺服器。礦池伺服器的所有者叫礦池管理員,同時他從礦工的收入中收取一個百分比的費用。

礦池伺服器執行專業軟體以及協調池中礦工們活動的礦池採礦協議。礦池伺服器同時也連線到一個或更多比特幣完全節點並直接訪問一個塊鏈資料庫的完整副本。這使得礦池伺服器可以代替礦池中的礦工驗證區塊和交易,緩解他們執行一個完整節點的負擔。對於池中的礦工,這是一個重要的考量,因為一個完整節點要求一個擁有最少15-20GB的永久儲存空間(磁碟)和最少2GB記憶體(RAM)的專用計算機。此外,執行一個完整節點的比特幣軟體需要監控、維護和頻繁升級。由於缺乏維護或資源導致的任何當機都會傷害到礦工的利潤。對於很多礦工來說,不需要跑一個完整節點就能採礦,也是加入托管礦池的一大好處。

礦工連線到礦池伺服器使用一個採礦協議比如Stratum (STM)或者 GetBlockTemplate (GBT)。一箇舊標準GetWork (GWK) 自從2012年底已經基本上過時了,因為它不支援在hash速度超過4GH/S時採礦。STM和GBT協議都建立包含候選區塊頭模板的區塊模板。礦池伺服器通過聚集交易,新增coinbase交易(和額外的隨機值空間),計算MERKLE根,並連線到上一個塊hash來建立一個候選區塊。這個候選區塊的頭部作為模板分發給每個礦工。礦工用這個區塊模板在低於比特幣網路的難度下采礦,併傳送成功的結果返回礦池伺服器賺取份額。

8.11.2.2 P2P礦池

託管礦池存在管理人作弊的可能,管理人可以利用礦池進行雙重支付或使區塊無效。(參見“8.12 共識攻擊”) 此外,中心化的礦池伺服器代表著單點故障。如果因為拒絕服務攻擊伺服器掛了或者被減慢,池中礦工就不能採礦。在2011年,為了解決由中心化造成的這些問題,提出和實施了一個新的礦池挖礦方法。P2Pool是一個點對點的礦池,沒有中心管理人。

P2Pool通過將礦池伺服器的功能去中心化,實現一個並行的類似區塊鏈的系統,名叫份額鏈。一個份額鏈是一個難度低於比特幣區塊鏈的區塊鏈系統。份額鏈允許池中礦工在一個去中心化的池中合作,以每30秒一個份額區塊的速度在份額鏈上採礦,並獲得份額。份額鏈上的區塊記錄了貢獻工作的礦工的份額,並且繼承了之前份額區塊上的份額記錄。當一個份額區塊上還實現了比特幣網路的難度目標時,它將被廣播幷包含到比特幣的區塊鏈上,並獎勵所有已經在份額鏈區塊中取得份額的池中礦工。本質上說,比起用一個礦池伺服器記錄礦工的份額和獎勵,份額鏈允許所有礦工通過類似比特幣區塊鏈系統的去中心化的共識機制跟蹤所有份額。

P2Pool採礦方式比在礦池中採礦要複雜的多,因為它要求礦工執行空間、記憶體、頻寬充足的專用計算機來支援一個比特幣的完整節點和P2Pool節點軟體。P2Pool礦工連線他們的採礦硬體到本地P2Pool節點,它通過傳送區塊模板到礦機來模擬一個礦池伺服器的功能。在P2Pool中,單獨的礦工建立自己的候選區塊,聚合交易,非常類似於solo礦工,但是他們在份額鏈上合作採礦。P2Pool是一種比單獨挖礦有更細粒度收入優勢的混合方法。但是不需要像託管礦池那樣給管理人太多權力。

最近,在集中式礦池已經接近產生51%攻擊的擔憂下,P2Pool的份額增長顯著。(參見參見“8.12 共識攻擊”)P2Pool協議的進一步發展有望去除對完整節點的需要,這將使去中心化採礦更容易。

8.12 共識攻擊

比特幣的共識機制指的是,被礦工(或礦池)試圖使用自己的算力實行欺騙或破壞的難度很大,至少理論上是這樣。就像我們前面講的,比特幣的共識機制依賴於這樣一個前提,那就是絕大多數的礦工,出於自己利益最大化的考慮,都會通過誠實地挖礦來維持整個比特幣系統。然而,當一個或者一群擁有了整個系統中大量算力的礦工出現之後,他們就可以通過攻擊比特幣的共識機制來達到破壞比特幣網路的安全性和可靠性的目的。

值得注意的是,共識攻擊只能影響整個區塊鏈未來的共識,或者說,最多能影響不久的過去幾個區塊的共識(最多影響過去10個塊)。而且隨著時間的推移,整個比特幣塊鏈被篡改的可能性越來越低。理論上,一個區塊鏈分叉可以變得很長,但實際上,要想實現一個非常長的區塊鏈分叉需要的算力非常非常大,隨著整個比特幣區塊鏈逐漸增長,過去的區塊基本可以認為是無法被分叉篡改的。同時,共識攻擊也不會影響使用者的私鑰以及加密演算法(ECDSA)。共識攻擊也不能從其他的錢包那裡偷到比特幣、不簽名地支付比特幣、重新分配比特幣、改變過去的交易或者改變比特幣持有紀錄。共識攻擊能夠造成的唯一影響是影響最近的區塊(最多10個)並且通過拒絕服務來影響未來區塊的生成。

共識攻擊的一個典型場景就是“51%攻擊”。想象這麼一個場景,一群礦工控制了整個比特幣網路51%的算力,他們聯合起來打算攻擊整個比特幣系統。由於這群礦工可以生成絕大多數的塊,他們就可以通過故意製造塊鏈分叉來實現“雙重支付”或者通過拒絕服務的方式來阻止特定的交易或者攻擊特定的錢包地址。區塊鏈分叉/雙重支付攻擊指的是攻擊者通過不承認最近的某個交易,並在這個交易之前重構新的塊,從而生成新的分叉,繼而實現雙重支付。有了充足算力的保證,一個攻擊者可以一次性篡改最近的6個或者更多的區塊,從而使得這些區塊包含的本應無法篡改的交易消失。值得注意的是,雙重支付只能在攻擊者擁有的錢包所發生的交易上進行,因為只有錢包的擁有者才能生成一個合法的簽名用於雙重支付交易。攻擊者只能在自己的交易上進行雙重支付攻擊,但當這筆交易對應的是不可逆轉的購買行為的時候,這種攻擊就是有利可圖的。

讓我們看一個“51%攻擊”的實際案例吧。在第1章我們講到,Alice 和 Bob 之間使用比特幣完成了一杯咖啡的交易。咖啡店老闆 Bob 願意在 Alice 給自己的轉賬交易確認數為零的時候就向其提供咖啡,這是因為這種小額交易遭遇“51%攻擊”的風險和顧客購物的即時性(Alice 能立即拿到咖啡)比起來,顯得微不足道。這就和大部分的咖啡店對低於25美元的信用卡消費不會費時費力地向顧客索要簽名是一樣的,因為和顧客有可能撤銷這筆信用卡支付的風險比起來,向使用者索要信用卡簽名的成本更高。相應的,使用比特幣支付的大額交易被雙重支付的風險就高得多了,因為買家(攻擊者)可以通過在全網廣播一個和真實交易的UTXO一樣的偽造交易,以達到取消真實交易的目的。雙重支付可以有兩種方式:要麼是在交易被確認之前,要麼攻擊者通過塊鏈分叉來完成。進行51%攻擊的人,可以取消在舊分叉上的交易記錄,然後在新分叉上重新生成一個同樣金額的交易,從而實現雙重支付。

再舉個例子:攻擊者Mallory在Carol的畫廊買了描繪偉大的中本聰的三聯組畫,Mallory通過轉賬價值25萬美金的比特幣與Carol進行交易。在等到一個而不是六個交易確認之後,Carol放心地將這幅組畫包好,交給了Mallory。這時,Mallory的一個同夥,一個擁有大量算力的礦池的人Paul,在這筆交易寫進區塊鏈的時候,開始了51%攻擊。首先,Paul利用自己礦池的算力重新計算包含這筆交易的塊,並且在新塊裡將原來的交易替換成了另外一筆交易(比如直接轉給了Mallory的另一個錢包而不是Carol的),從而實現了“雙重支付”。這筆“雙重支付”交易使用了跟原有交易一致的UTXO,但收款人被替換成了Mallory的錢包地址。然後,Paul利用礦池在偽造的塊的基礎上,又計算出一個更新的塊,這樣,包含這筆“雙重支付”交易的塊鏈比原有的塊鏈高出了一個塊。到此,高度更高的分叉區塊鏈取代了原有的區塊鏈,“雙重支付”交易取代了原來給Carol的交易,Carol既沒有收到價值25萬美金的比特幣,原本擁有的三幅價值連城的畫也被Mallory白白拿走了。在整個過程中,Paul礦池裡的其他礦工可能自始至終都沒有覺察到這筆“雙重支付”交易有什麼異樣,因為挖礦程式都是自動在執行,並且不會時時監控每一個區塊中的每一筆交易。

為了避免這類攻擊,售賣大宗商品的商家應該在交易得到全網的6個確認之後再交付商品。或者,商家應該使用第三方的多方簽名的賬戶進行交易,並且也要等到交易賬戶獲得全網多個確認之後再交付商品。一條交易的確認數越多,越難被攻擊者通過51%攻擊篡改。對於大宗商品的交易,即使在付款24小時之後再發貨,對買賣雙方來說使用比特幣支付也是方便並且有效率的。而24小時之後,這筆交易的全網確認數將達到至少144個(能有效降低被51%攻擊的可能性)。

共識攻擊中除了“雙重支付”攻擊,還有一種攻擊場景就是拒絕對某個特定的比特幣地址提供服務。一個擁有了系統中絕大多數算力的攻擊者,可以輕易地忽略某一筆特定的交易。如果這筆交易存在於另一個礦工所產生的區塊中,該攻擊者可以故意分叉,然後重新產生這個區塊,並且把想忽略的交易從這個區塊中移除。這種攻擊造成的結果就是,只要這名攻擊者擁有系統中的絕大多數算力,那麼他就可以持續地干預某一個或某一批特定錢包地址產生的所有交易,從而達到拒絕為這些地址服務的目的。

需要注意的是,51%攻擊並不是像它的命名裡說的那樣,攻擊者需要至少51%的算力才能發起,實際上,即使其擁有不到51%的系統算力,依然可以嘗試發起這種攻擊。之所以命名為51%攻擊,只是因為在攻擊者的算力達到51%這個閾值的時候,其發起的攻擊嘗試幾乎肯定會成功。本質上來看,共識攻擊,就像是系統中所有礦工的算力被分成了兩組,一組為誠實算力,一組為攻擊者算力,兩組人都在爭先恐後地計算塊鏈上的新塊,只是攻擊者算力算出來的是精心構造的、包含或者剔除了某些交易的塊。因此,攻擊者擁有的算力越少,在這場決逐中獲勝的可能性就越小。從另一個角度講,一個攻擊者擁有的算力越多,其故意創造的分叉塊鏈就可能越長,可能被篡改的最近的塊或者或者受其控制的未來的塊就會越多。一些安全研究組織利用統計模型得出的結論是,算力達到全網的30%就足以發動51%攻擊了。

全網算力的急劇增長已經使得比特幣系統不再可能被某一個礦工攻擊,因為一個礦工已經不可能佔據全網哪怕的1%算力。但是中心化控制的礦池則引入了礦池操作者出於利益而施行攻擊的風險。礦池操作者控制了候選塊的生成,同時也控制哪些交易會被放到新生成的塊中。這樣一來,礦池操作者就擁有了剔除特定交易或者雙重支付的權力。如果這種權利被礦池操作者以微妙而有節制的方式濫用的話,那麼礦池操作者就可以在不為人知的情況下發動共識攻擊並獲益。

但是,並不是所有的攻擊者都是為了利益。一個可能的場景就是,攻擊者僅僅是為了破壞整個比特幣系統而發動攻擊,而不是為了利益。這種意在破壞比特幣系統的攻擊者需要巨大的投入和精心的計劃,因此可以想象,這種攻擊很有可能來自政府資助的組織。同樣的,這類攻擊者或許也會購買礦機,運營礦池,通過濫用礦池操作者的上述權力來施行拒絕服務等共識攻擊。但是,隨著比特幣網路的算力呈幾何級數快速增長,上述這些理論上可行的攻擊場景,實際操作起來已經越來越困難。近期比特幣系統的一些升級,比如旨在進一步將挖礦控制去中心化的P2Pool挖礦協議,也都正在讓這些理論上可行的攻擊變得越來越困難。

毫無疑問,一次嚴重的共識攻擊事件勢必會降低人們對比特幣系統的信心,進而可能導致比特幣價格的跳水。然而,比特幣系統和相關軟體也一直在持續改進,所以比特幣社群也勢必會對任何一次共識攻擊快速做出響應,以使整個比特幣系統比以往更加穩健和可靠。

相關文章