用Go语言建立一个简单的区块链part2:Pow共识
工作量證明
在上一節,我們構造了一個非常簡單的數據結構 – 區塊,它也是整個區塊鏈數據庫的核心。目前所完成的區塊鏈原型,已經可以通過鏈式關系把區塊相互關聯起來:每個塊都與前一個塊相關聯。
但是,當前實現的區塊鏈有一個巨大的缺陷:向鏈中加入區塊太容易,也太廉價了。而區塊鏈和比特幣的其中一個核心就是,要想加入新的區塊,必須先完成一些非常困難的工作。在本文,我們將會彌補這個缺陷。
工作量證明
區塊鏈的一個關鍵點就是,一個人必須經過一系列困難的工作,才能將數據放入到區塊鏈中。正是由于這種困難的工作,才保證了區塊鏈的安全和一致。此外,完成這個工作的人,也會獲得相應獎勵(這也就是通過挖礦獲得幣)。
這個機制與生活現象非常類似:一個人必須通過努力工作,才能夠獲得回報或者獎勵,用以支撐他們的生活。在區塊鏈中,是通過網絡中的參與者(礦工)不斷的工作來支撐起了整個網絡。礦工不斷地向區塊鏈中加入新塊,然后獲得相應的獎勵。在這種機制的作用下,新生成的區塊能夠被安全地加入到區塊鏈中,它維護了整個區塊鏈數據庫的穩定性。值得注意的是,完成了這個工作的人必須要證明這一點,即他必須要證明他的確完成了這些工作。
整個 “努力工作并進行證明” 的機制,就叫做工作量證明(proof-of-work)。要想完成工作非常地不容易,因為這需要大量的計算能力:即便是高性能計算機,也無法在短時間內快速完成。另外,這個工作的困難度會隨著時間不斷增長,以保持每 10 分鐘出 1 個新塊的速度。在比特幣中,這個工作就是找到一個塊的哈希,同時這個哈希滿足了一些必要條件。這個哈希,也就充當了證明的角色。因此,尋求證明(尋找有效哈希),就是礦工實際要做的事情。
哈希計算
在本節,我們會討論哈希計算。如果你已經熟悉了這個概念,可以直接跳過。
獲得指定數據的一個哈希值的過程,就叫做哈希計算。一個哈希,就是對所計算數據的一個唯一表示。對于一個哈希函數,輸入任意大小的數據,它會輸出一個固定大小的哈希值。
1、無法從一個哈希值恢復原始數據。也就是說,哈希并不是加密。
2、對于特定的數據,只能有一個哈希,并且這個哈希是唯一的。
3、即使是僅僅改變輸入數據中的一個字節,也會導致輸出一個完全不同的哈希。
哈希函數被廣泛用于檢測數據的一致性。軟件提供者常常在除了提供軟件包以外,還會發布校驗和。當下載完一個文件以后,你可以用哈希函數對下載好的文件計算一個哈希,并與作者提供的哈希進行比較,以此來保證文件下載的完整性。
在區塊鏈中,哈希被用于保證一個塊的一致性。哈希算法的輸入數據包含了前一個塊的哈希,因此使得不太可能(或者,至少很困難)去修改鏈中的一個塊:因為如果一個人想要修改前面一個塊的哈希,那么他必須要重新計算這個塊以及后面所有塊的哈希。
Hashcash
比特幣使用 Hashcash ,一個最初用來防止垃圾郵件的工作量證明算法。它可以被分解為以下步驟:
1、取一些公開的數據(比如,如果是 email 的話,它可以是接收者的郵件地址;在比特幣中,它是區塊頭)
2、給這個公開數據添加一個計數器。計數器默認從 0 開始
3、將 data(數據) 和 counter(計數器) 組合到一起,獲得一個哈希
4、檢查哈希是否符合一定的條件:
4.1、如果符合條件,結束
4.2、如果不符合,增加計數器,重復步驟 3-4
因此,這是一個暴力算法:改變計數器,計算新的哈希,檢查,增加計數器,計算哈希,檢查,如此往復。這也是為什么說它的計算成本很高,因為這一步需要如此反復不斷地計算和檢查。
現在,讓我們來仔細看一下一個哈希要滿足的必要條件。在原始的 Hashcash 實現中,它的要求是 “一個哈希的前 20 位必須是 0”。在比特幣中,這個要求會隨著時間而不斷變化。因為按照設計,必須保證每 10 分鐘生成一個塊,而不論計算能力會隨著時間增長,或者是會有越來越多的礦工進入網絡,所以需要動態調整這個必要條件。
為了闡釋這一算法,我從前一個例子(“I like donuts”)中取得數據,并且找到了一個前 3 個字節是全是 0 的哈希。
ca07ca 是計數器的 16 進制值,十進制的話是 13240266.
實現
好了,完成了理論層面,來動手寫代碼吧!首先,定義挖礦的難度值:
在比特幣中,當一個塊被挖出來以后,“target bits” 代表了區塊頭里存儲的難度,也就是開頭有多少個 0。這里的 24 指的是算出來的哈希前 24 位必須是 0,如果用 16 進制表示,就是前 6 位必須是 0,這一點從最后的輸出可以看出來。目前我們并不會實現一個動態調整目標的算法,所以將難度定義為一個全局的常量即可。
24 其實是一個可以任意取的數字,其目的只是為了有一個目標(target)而已,這個目標占據不到 256 位的內存空間。同時,我們想要有足夠的差異性,但是又不至于大的過分,因為差異性越大,就越難找到一個合適的哈希。
type ProofOfWork struct {block *Blocktarget *big.Int }func NewProofOfWork(b *Block) *ProofOfWork {target := big.NewInt(1)target.Lsh(target, uint(256-targetBits))pow := &ProofOfWork{b, target}return pow }這里,我們構造了 ProofOfWork 結構,里面存儲了指向一個塊(block)和一個目標(target)的指針。這里的 “目標” ,也就是前一節中所描述的必要條件。這里使用了一個 大整數 ,我們會將哈希與目標進行比較:先把哈希轉換成一個大整數,然后檢測它是否小于目標。
在 NewProofOfWork 函數中,我們將 big.Int 初始化為 1,然后左移 256 - targetBits 位。256 是一個 SHA-256 哈希的位數,我們將要使用的是 SHA-256 哈希算法。target(目標) 的 16 進制形式為:
0x10000000000000000000000000000000000000000000000000000000000它在內存上占據了 29 個字節。下面是與前面例子哈希的形式化比較:
0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3 0000010000000000000000000000000000000000000000000000000000000000 0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca第一個哈希(基于 “I like donuts” 計算)比目標要大,因此它并不是一個有效的工作量證明。第二個哈希(基于 “I like donutsca07ca” 計算)比目標要小,所以是一個有效的證明。
譯者注:上面的形式化比較有些“言不符實”,其實它應該并非由 “I like donuts” 而來,但是原文表達的意思是沒問題的,可能是疏忽而已。下面是我做的一個小實驗:
package mainimport ("crypto/sha256""fmt""math/big" )func main() {data1 := []byte("I like donuts")data2 := []byte("I like donutsca07ca")targetBits := 24target := big.NewInt(1)target.Lsh(target, uint(256-targetBits))fmt.Printf("%x\n", sha256.Sum256(data1))fmt.Printf("%64x\n", target)fmt.Printf("%x\n", sha256.Sum256(data2))}輸出:
你可以把目標想象為一個范圍的上界:如果一個數(由哈希轉換而來)比上界要小,那么是有效的,反之無效。因為要求比上界要小,所以會導致有效數字并不會很多。因此,也就需要通過一些困難的工作(一系列反復地計算),才能找到一個有效的數字。
現在,我們需要有數據來進行哈希,準備數據:
func (pow *ProofOfWork) prepareData(nonce int) []byte {data := bytes.Join([][]byte{pow.block.PrevBlockHash,pow.block.Data,IntToHex(pow.block.Timestamp),IntToHex(int64(targetBits)),IntToHex(int64(nonce)),},[]byte{},)return data }這個部分比較直觀:只需要將 target ,nonce 與 Block 進行合并。這里的 nonce,就是上面 Hashcash 所提到的計數器,它是一個密碼學術語。
很好,到這里,所有的準備工作就完成了,下面來實現 PoW 算法的核心:
func (pow *ProofOfWork) Run() (int, []byte) {var hashInt big.Intvar hash [32]bytenonce := 0fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)for nonce < maxNonce {data := pow.prepareData(nonce)hash = sha256.Sum256(data)hashInt.SetBytes(hash[:])if hashInt.Cmp(pow.target) == -1 {fmt.Printf("\r%x", hash)break} else {nonce++}}fmt.Print("\n\n")return nonce, hash[:] }首先我們對變量進行初始化:
HashInt 是 hash 的整形表示;
nonce 是計數器。
然后開始一個 “無限” 循環:maxNonce 對這個循環進行了限制, 它等于 math.MaxInt64,這是為了避免 nonce 可能出現的溢出。盡管我們 PoW 的難度很小,以至于計數器其實不太可能會溢出,但最好還是以防萬一檢查一下。
在這個循環中,我們做的事情有:
1、準備數據
2、用 SHA-256 對數據進行哈希
3、將哈希轉換成一個大整數
4、將這個大整數與目標進行比較
跟之前所講的一樣簡單。現在我們可以移除 Block 的 SetHash 方法,然后修改 NewBlock 函數:
func NewBlock(data string, prevBlockHash []byte) *Block {block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}pow := NewProofOfWork(block)nonce, hash := pow.Run()block.Hash = hash[:]block.Nonce = noncereturn block }在這里,你可以看到 nonce 被保存為 Block 的一個屬性。這是十分有必要的,因為待會兒我們對這個工作量進行驗證時會用到 nonce 。Block 結構現在看起來像是這樣:
type Block struct {Timestamp int64Data []bytePrevBlockHash []byteHash []byteNonce int }好了!現在讓我們來運行一下是否正常工作:
Mining the block containing "Genesis Block" 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1Mining the block containing "Send 1 BTC to Ivan" 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804Mining the block containing "Send 2 more BTC to Ivan" 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbePrev. hash: Data: Genesis Block Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1 Data: Send 1 BTC to Ivan Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804 Data: Send 2 more BTC to Ivan Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe成功了!你可以看到每個哈希都是 3 個字節的 0 開始,并且獲得這些哈希需要花費一些時間。
還剩下一件事情需要做,對工作量證明進行驗證:
func (pow *ProofOfWork) Validate() bool {var hashInt big.Intdata := pow.prepareData(pow.block.Nonce)hash := sha256.Sum256(data)hashInt.SetBytes(hash[:])isValid := hashInt.Cmp(pow.target) == -1return isValid }這里,就是我們就用到了上面保存的 nonce。
再來檢測一次是否正常工作:
func main() {...for _, block := range bc.blocks {...pow := NewProofOfWork(block)fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))fmt.Println()} }輸出:
Prev. hash: Data: Genesis Block Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038 PoW: truePrev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038 Data: Send 1 BTC to Ivan Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b PoW: truePrev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b Data: Send 2 more BTC to Ivan Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a PoW: true從下圖可以看出,這次我們產生三個塊花費了一分多鐘,比沒有工作量證明之前慢了很多(也就是成本高了很多):
總結
我們離真正的區塊鏈又進了一步:現在需要經過一些困難的工作才能加入新的塊,因此挖礦就有可能了。但是,它仍然缺少一些至關重要的特性:區塊鏈數據庫并不是持久化的,沒有錢包,地址,交易,也沒有共識機制。不過,所有的這些,我們都會在接下來的文章中實現,現在,愉快地挖礦吧!
總結
以上是生活随笔為你收集整理的用Go语言建立一个简单的区块链part2:Pow共识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用Go语言建立一个简单的区块链part1
- 下一篇: 用Go语言建立一个简单的区块链part3