天天看點

[區塊鍊研究實驗室]區塊鍊之工作量證明(POW)

比特币網絡是公開的,是以共識協定的穩定性和防攻擊性十分關鍵。

比特币區塊鍊采用了 Proof of Work(PoW)的機制來實作共識,該機制于 1998 年在 B-money 設計中提出。

目前,Proof of 系列中比較出名的一緻性協定包括 PoW 和 PoS,都是通過經濟懲罰來限制惡意參與。

POW工作量證明

工作量證明,Proof of Work,通過計算來猜測一個數值(nonce),得以解決規定的 hash 問題(來源于 hashcash)。保證在一段時間内,系統中隻能出現少數合法提案。

同時,這些少量的合法提案會在網絡中進行廣播,收到的使用者進行驗證後會基于它認為的最長鍊上繼續難題的計算。是以,系統中可能出現鍊的分叉(Fork),但最終會有一條鍊成為最長的鍊。

hash 問題具有不可逆的特點,是以,目前除了暴力計算外,還沒有有效的算法進行解決。反之,如果獲得符合要求的 nonce,則說明在機率上是付出了對應的算力。誰的算力多,誰最先解決問題的機率就越大。當掌握超過全網一半算力時,從機率上就能控制網絡中鍊的走向。這也是所謂 51% 攻擊 的由來。

參與 PoW 計算比賽的人,将付出不小的經濟成本(硬體、電力、維護等)。當沒有成為首個算出的“幸運兒”時,這些成本都将被沉沒掉。這也保障了,如果有人惡意破壞,需要付出大量的經濟成本。也有設計試圖将後算出結果者的算力按照一定比例折合進下一輪比賽考慮。

有一個很直覺的例子可以說明為何這種經濟博弈模式會確定系統中最長鍊的唯一。

Hash哈希計算

在本節,我們會讨論哈希計算。如果你已經熟悉了這個概念,可以直接跳過。

獲得指定資料的一個哈希值的過程,就叫做哈希計算。一個哈希,就是對所計算資料的一個唯一表示。對于一個哈希函數,輸入任意大小的資料,它會輸出一個固定大小的哈希值。

下面是哈希的幾個關鍵特性:

  1. 無法從一個哈希值恢複原始資料。也就是說,哈希并不是加密。
  2. 對于特定的資料,隻能有一個哈希,并且這個哈希是唯一的。
  3. 即使是僅僅改變輸入資料中的一個位元組,也會導緻輸出一個完全不同的哈希。
[區塊鍊研究實驗室]區塊鍊之工作量證明(POW)

哈希函數被廣泛用于檢測資料的一緻性。軟體提供者常常在除了提供軟體包以外,還會釋出校驗和。當下載下傳完一個檔案以後,你可以用哈希函數對下載下傳好的檔案計算一個哈希,并與作者提供的哈希進行比較,以此來保證檔案下載下傳的完整性。

在區塊鍊中,哈希被用于保證一個塊的一緻性。雜湊演算法的輸入資料包含了前一個塊的哈希,是以使得不太可能(或者,至少很困難)去修改鍊中的一個塊:因為如果一個人想要修改前面一個塊的哈希,那麼他必須要重新計算這個塊以及後面所有塊的哈希。

HashCash

比特币使用 Hashcash ,一個最初用來防止垃圾郵件的工作量證明算法。它可以被分解為以下步驟:

  1. 取一些公開的資料(比如,如果是 email 的話,它可以是接收者的郵件位址;在比特币中,它是區塊頭)
  2. 給這個公開資料添加一個計數器。計數器預設從 0 開始
  3. 将 data(資料) 和 counter(計數器) 組合到一起,獲得一個哈希
  4. 檢查哈希是否符合一定的條件:
  •    如果符合條件,結束
  •    如果不符合,增加計數器,重複步驟 3-4

是以,這是一個暴力算法:改變計數器,計算新的哈希,檢查,增加計數器,計算哈希,檢查,如此往複。這也是為什麼說它的計算成本很高,因為這一步需要如此反複不斷地計算和檢查。

現在,讓我們來仔細看一下一個哈希要滿足的必要條件。在原始的 Hashcash 實作中,它的要求是 “一個哈希的前 20 位必須是 0”。在比特币中,這個要求會随着時間而不斷變化。因為按照設計,必須保證每 10 分鐘生成一個塊,而不論計算能力會随着時間增長,或者是會有越來越多的礦工進入網絡,是以需要動态調整這個必要條件。

為了闡釋這一算法,我從前一個例子(“I like donuts”)中取得資料,并且找到了一個前 3 個位元組是全是 0 的哈希。

[區塊鍊研究實驗室]區塊鍊之工作量證明(POW)

ca07ca 是計數器的 16 進制值,十進制的話是 13240266.

實戰操作

好了,完成了理論層面,來動手寫代碼吧!首先,定義挖礦的難度值:

const targetBits = 24

在比特币中,當一個塊被挖出來以後,“target bits” 代表了區塊頭裡存儲的難度,也就是開頭有多少個 0。這裡的 24 指的是算出來的哈希前 24 位必須是 0,如果用 16 進制表示,就是前 6 位必須是 0,這一點從最後的輸出可以看出來。目前我們并不會實作一個動态調整目标的算法,是以将難度定義為一個全局的常量即可。

24 其實是一個可以任意取的數字,其目的隻是為了有一個目标(target)而已,這個目标占據不到 256 位的記憶體空間。同時,我們想要有足夠的差異性,但是又不至于大的過分,因為差異性越大,就越難找到一個合适的哈希。

type ProofOfWork struct {    

block  *Block    

target *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 main

import (    

"crypto/sha256"    

"fmt"    

"math/big"

)

func main() {    

data1 := []byte("I like donuts")    

data2 := []byte("I like donutsca07ca")    

targetBits := 24    target := 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))

}

輸出:

[區塊鍊研究實驗室]區塊鍊之工作量證明(POW)

你可以把目标想象為一個範圍的上界:如果一個數(由哈希轉換而來)比上界要小,那麼是有效的,反之無效。因為要求比上界要小,是以會導緻有效數字并不會很多。是以,也就需要通過一些困難的工作(一系列反複地計算),才能找到一個有效的數字。

現在,我們需要有資料來進行哈希,準備資料:

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.Int    

var hash [32]byte    

nonce := 0    

fmt.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 = nonce    

return block

}

在這裡,你可以看到 nonce 被儲存為 Block 的一個屬性。這是十分有必要的,因為待會兒我們對這個工作量進行驗證時會用到 nonce 。Block 結構現在看起來像是這樣:

type Block struct {  

 Timestamp     int64  

 Data          []byte    

PrevBlockHash []byte    

Hash          []byte    

Nonce         int

}

好了!現在讓我們來運作一下是否正常工作:

Mining the block containing "Genesis Block"

00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Mining the block containing "Send 1 BTC to Ivan"

00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Mining the block containing "Send 2 more BTC to Ivan"

000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Prev. hash:

Data: Genesis Block

Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Data: Send 1 BTC to Ivan

Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Data: Send 2 more BTC to Ivan

Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

成功了!你可以看到每個哈希都是 3 個位元組的 0 開始,并且獲得這些哈希需要花費一些時間。

還剩下一件事情需要做,對工作量證明進行驗證:

func (pow *ProofOfWork) Validate() bool {    

var hashInt big.Int    

data := pow.prepareData(pow.block.Nonce)    

hash := sha256.Sum256(data)    

hashInt.SetBytes(hash[:])    

isValid := hashInt.Cmp(pow.target) == -1    

return 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: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038

Data: Send 1 BTC to Ivan

Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b

PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b

Data: Send 2 more BTC to Ivan

Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a

PoW: true

從下圖可以看出,這次我們産生三個塊花費了一分多鐘,比沒有工作量證明之前慢了很多(也就是成本高了很多):

[區塊鍊研究實驗室]區塊鍊之工作量證明(POW)

總結

我們離真正的區塊鍊又進了一步:現在需要經過一些困難的工作才能加入新的塊,是以挖礦就有可能了。但是,它仍然缺少一些至關重要的特性:區塊鍊資料庫并不是持久化的,沒有錢包,位址,交易,也沒有共識機制。不過,所有的這些,我們都會在接下來的文章中實作,現在,愉快地挖礦吧!

歡迎關注公衆号:區塊鍊研究實驗室

繼續閱讀