2.2 工作量證明
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 { Timestampint64 Data[]byte PrevBlockHash []byte Hash[]byte Nonceint }
然後執行 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中,我們會持續完善。
-
學院Go語言視訊主頁
ofollow,noindex" target="_blank">https://edu.csdn.net/lecturer/1928 -
掃碼獲取海量視訊及原始碼 QQ群:721929980