2.2 工作量證明

尹成發表於2018-11-08

Proof-of-Work 工作量證明

區塊裡有一個非常關鍵的點,就是節點必須執行足夠多且困難的運算才能將資料新增在區塊中。這一困難的運算保證了區塊鏈安全、一致。而為了獎勵這一運算,該節點會獲得數字貨幣(如比特幣)的獎勵(從運算到收到獎勵的過程,也叫作挖礦)。 
這一機制和現實生活中也是相似的:人們辛苦工作獲取報酬來維持生活,在區塊鏈中,鏈絡中的參與者(比如礦工)辛苦運算來維繫這個區塊鏈網路,不斷增加新的區塊到鏈絡中,然後獲取回報。正是因為這些運算,新的區塊基於安全的方式加到區塊鏈中,保證了區塊鏈資料庫的穩定。 
是不是發現了什麼問題呢?大家都在計算,憑什麼怎麼證明你做的運算就是對的,且是你的。 
努力工作並證明(do hard work and prove),這一機制被稱為工作量證明。需要多努力呢,需要大量計算機資源,即使使用高速計算機也不能做得快多少。而且,隨著時間的推移,難度會越來越大,因為要保證每小時有6個區塊的誕生,越到後面,區塊越來越少,要保證這個速率,只能運算更多,提高難度。在比特幣中,運算的目標是計算出一串符合要求的hash值。而這個hash就是證明。所以說,找到證明(符合要求的hash值)才是實際意義上的工作。 
工作量證明還有一個重要知識點。也即工作困難,而證明容易。因為如果你的工作困難,而證明也困難,那麼你的工作在圈子效率意義就不大,對於需要提供給別人證明的工作,別人證明起來越簡單就越好。

Hash 雜湊

Hash運算是區塊鏈最主要使用的工作演算法。雜湊運算是指給特殊資料計算一串hash字元的過程。對於一筆資料而言,它的hash值是唯一的。hash函式就是可以把任意大小的資料計算出指定大小hash值的函式。 
Hash運算有以下幾個主要的特點: 
1. 原始資料不能從hash值中逆向計算得到 
2. 確定的資料只有一個hash值且這個hash值是唯一的 
3. 改變資料的任一byte都會造成hash值變動

Hash運算廣泛應用於資料一致性的驗證。很多線上軟體商店都會把軟體包的hash值公開,使用者下載後自行計算hash值後驗證和供應商的是否一致,就可判斷軟體是否被篡改過。 
在區塊鏈中,hash也是用於保證資料的一致性。區塊的資料,還有區塊的前一區塊的hash值也會被用於計算本區塊的hash值,這樣就保證每個區塊是不可變:如果有人要改動自己區塊的hash值,那麼連他後面的區塊hash也要跟著改,這顯然是不可能的或者極其困難的(要說服不是自己的區塊一同更改很困難)。

Hashcash 雜湊現金

比特幣中的工作證明使用的是Hashcash技術,起初,這一演算法開發出來就是用於防止垃圾電子郵件。它的工作主要有以下幾個步驟: 
1. 獲取公開的資訊,比如郵件的收件人地址或者比特幣的頭部資訊 
2. 增加一個起始值為0的計數器 
3. 計算出第1步中的資訊+計數器值組合的hash值 
4. 按規則檢測hash值是否滿足需求(一般是指定前20位是0) 
1. 滿足 
2. 不滿足則重複3-4步驟 
這個演算法看上去比較暴力:改變計數器值,計算新的hash,檢測,增加計數器值,計算新的hash…,所以這個演算法比較昂貴。 
郵件傳送者預備好郵件頭部資訊然後附加用於計算隨機數字計數值。然後計算160-bit長的hash頭,如果前20bits 
現在進一步分析區塊鏈hash運算的要求。在原始的hashcash實現中,必須根據頭資訊算出前20位為0的hash值。而在比特幣中,這一規則則是根據時間的推移變動的,因為比特幣的設計就是10分鐘出一塊新區塊,即使計算機算力提升或者更多的礦工加入挖礦行列中也不會改變,也就是說,算出hash值,會越來越困難。 
下面演示這一演算法,和上面第一張圖一樣使用“I like donuts”作為資料,再在資料後加面附加計數器,然後使用SHA256演算法找出前面6位為0的hash值。

而ca07ca就是不停運算找到的計數器值,轉換成10進位制就是13240266,換言之大概執行了13240266次SHA256運算才找到符合條件的值。

實現

上面花了點篇幅介紹了工作量證明的原理。現在我們用Golang來實現。先定24位0的作為挖礦的難度: 
const targetBits = 24 
注:在比特幣挖礦中,頭部中的target bits儲存該區塊的挖礦難度,但是上面說過隨著時間推移難度越來越大,所以這個target大小是會變的,這裡不實現target的適配演算法,這不影響我們理解挖礦。現在只定義一個常量作為全域性的難度。 
當然,24也是比較隨意的,我們只用在記憶體中佔用少於256bits的的空間。差異也要大些,但是也不要太大,太大了就就很難找出來一個合規的hash。 
然後定義ProofOfWork結構:

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”兩個成員。“target”就是上面段落中描述的hashcash的規則資訊。使用big.Int是因為要把hash轉成大整數,然後檢測是否比target要小。

NewProofOfWork 函式負責初始化target,將1往左偏移(256-targetBits)位。256是我們要使用的SHA-256標準的hash值長度。轉換成16進位制就是:

0x10000000000000000000000000000000000000000000000000000000000

這會佔用29位大小的記憶體空間。

現在準備建立hash的函式:

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
}

這段程式碼做了簡化,直接把block的資訊和target、nonce合併在一起。nonce就是Hashcash中的counter,nonce(現時標誌)是加密的術語。 
準備工作都OK了,現在實現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)
    fmt.Printf("\r%x", hash)
    hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
      break
    } else {
      nonce++
    }
  }
  fmt.Print("\n\n")
return nonce, hash[:]
}

hashInt是hash值的int形式,nonce是計數器。然後執行math.MaxInt64次迴圈直到找符合target的hash值。為什麼是math.MaxInt64次,其實這個例子中是不用的考慮這麼大的,因為我們示例中的PoW太小了以致於還不會造成溢位,只是程式設計上要考慮一下,當心為上。 
迴圈體內工作主要是: 
1. 準備塊資料 
2. 計算SHA-256值 
3. 轉成big int 
4. 與target比較 
將上一篇中的NewBlock方法改造一下,扔掉SetHash方法:

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
}

然後執行 go run *.go

start: 2018-03-07 14:43:31.691959 +0800 CST m=+0.000721510
Mining the block containing "Genesis Block"
0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
end:  2018-03-07 14:44:32.488829 +0800 CST m=+60.798522580
elapsed time:  1m0.797798933s
start: 2018-03-07 14:44:32.489057 +0800 CST m=+60.798750578
Mining the block containing "Send 1 BTC to Ivan"
000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
end:  2018-03-07 14:46:32.996032 +0800 CST m=+181.307571128
elapsed time:  2m0.508818203s
start: 2018-03-07 14:46:32.996498 +0800 CST m=+181.308036527
Mining the block containing "Send 2 more BTC to Ivan"
0000001927685501c59f28c0bda3fdd0472e88d6eec3822b0ab98e5b1c28c676
end:  2018-03-07 14:46:53.90008 +0800 CST m=+202.211938702
elapsed time:  20.903900066s

可以看到生成了前6位都是0的hash字串,因為是16進位制的,就是24一位,共4*6=24位,也就是我們設定的targetBits。從時間上可以看到,計算出hashcash的時間有一定的隨機性,多著2分,少則20秒。 
現在還需要去驗證是否是正確的。

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在驗證是要用到的,告訴對方也用我們計算出來的資料進行二次計算,如果對方計算的符合要求,說明我們的計算合法。 
把驗證方法加到main函式中:

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: 0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
PoW: true
Prev. hash: 0000006f3387b588739cbcfe2cce521fcce27d4306776039e02c2904b116ab9a
Data: Send 1 BTC to Ivan
Hash: 000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
PoW: true
Prev. hash: 000000e9d5a266faa6a86f56a36ea09212ecad28e524a8b0599589fd5b800d13
Data: Send 2 more BTC to Ivan
Hash: 0000001927685501c59f28c0bda3fdd0472e88d6eec3822b0ab98e5b1c28c676
PoW: true

本章總結 
本章我們的區塊鏈進一步接近實際的結構:增加了計算難度,這意味著挖礦成為可能。不過還是欠缺了一些特性,比如沒有把計算出來的資料持久化,沒有錢包(wallet)、地址、交易,以及實現一致性機制。接下來的幾篇abc中,我們會持續完善。

 

相關文章