區塊鏈poW 工作量證明
挖礦原理
在講poW之前我們先來講講以比特幣為例的挖礦原理,其實說挖礦其實並不準確,我們應該稱其為記賬。記賬是把交易記錄、交易時間、賬本序號、上一個Hash值等資訊計算Hash打包的過程。這一過程必然需要某個計算機來實現,這類計算機我們下面統稱為“節點”。因為區塊鏈是分散式的,所以就需要很多節點,而且計算需要消耗很多資源。如果沒有一點獎勵機制,大家肯定都不情願,把自己的裝置作為免費節點。因此在比特幣中,中本聰設定了只要完成記賬的節點給予一定的比特幣作為獎勵,因此大家把記賬形象的稱為“挖礦”。
記賬原理
上面的我說了,成功的完成記賬後就會有比特幣獎勵,因此就出現大家爭相記賬,大家一起記賬就會引起問題:出現記賬不一致的問題,怎麼判斷你的記賬是正確的。比特幣系統引入工作量證明來解決這個問題,規則如下:
1.一段時間內只有一人可以記賬成功
2.通過解決密碼學難題(即工作量證明)競爭獲得唯一記賬權
3.其他節點複製記賬結果那麼我們通過什麼來判斷你完成了記賬。
在進行工作量證明之前,記賬的節點會做一些工作:
收集廣播中海沒有被記錄的原始交易資訊
檢查交易資訊中付款地址有沒有足夠的餘額
驗證交易是否有正確的簽名
把驗證通過的交易資訊進行打包記錄
新增一個獎勵交易:給自己的地址增加特幣
poW
poW全稱Proof-of-Work 即工作量證明。通過記賬原理我們知道,每次記賬就是把上一個塊的Hash值和當前塊的資訊一起作為原始資訊進行Hash。如果僅僅這麼輕鬆點完成了記賬,相信比特幣也不值錢。因此為了保證一段時間內只有一個人能完成記賬,就需要提高記賬難度。而且,隨著時間的推移,難度會越來越大,因為要保證每小時有6個區塊的誕生,越到後面,區塊越來越少,要保證這個速率只能運算更多,提高難度。在比特幣中,運算的目標是計算出一串符合要求的hash值。而這個hash就是證明。所以說,找到證明(符合要求的hash值)才是實際意義上的工作。
我們知道改變Hash的原始資訊的任何一部分,Hash值也會隨之不斷的變化,因此在運算Hash時,不斷的改變隨機數的值,總可以找的一個隨機數使的Hash的結果以若干個0開頭(下文把這個過程稱為猜謎),率先找到隨機數的節點就獲得此次記賬的唯一記賬權。Hash值示例:
00000017504a984ab0339f7d1517691e63099fb83d6ae9d6ebd722c25755f2cc
1
計算量分析
以我寫這篇檔案的時間來看(2018-10-4),挖一個比特幣的成本需要3W多人民幣。為什麼成本這麼高呢?我們通過比特幣來簡單分析下挖礦難度有多大。Hash值是由數字和大小寫字母構成的字串,每一位有62種可能性,假設任何一個字元出現的概率是均等的,那麼第一位為0的概率是1/62(其他位出現什麼字元先不管),理論上需要嘗試62次Hash運算才會出現一次第一位為0的情況,如果前兩2位為0,就得嘗試62的平方次Hash運算,以n個0開頭就需要嘗試62的n次方次運算。
我們以Block #544228為例,它當前hash為:000 000 000 000 000 000 14d90bb35d84eaf8b5ed2ae85f42b9d37c715ab3a02b06。 可以看到它前面有24個0,那麼理論上就需要嘗試 64^24 ,大概需要2.230074519853062e43 次這是一個非常大的數字,需要消耗很大的資源(電能、算力),所以比特幣目前來說非常難挖。
設計邏輯
poW區塊設計
poW結構設計
實現工作量證明
區塊鏈構建
驗證區塊
實現poW
上面花了點篇幅介紹了工作量證明的原理。現在我們根據上篇的內容進行修改,來實現poW。
定義挖礦難度
先定義20位0作為挖礦難度,我們這裡的難度是全域性的,並且不作改變。
ps:在實際區塊鏈中,targetBit是變化的,挖礦是隨著時間變的越來越難。
const targetBit =20
因為我們這裡只做演示,所以targetBit不能太大,不然會消耗很多時間。
區塊結構
相比於上篇的程式碼我們添加了一個新的屬性 Nonce ,用於生成工作量證明的雜湊。
type Block struct {
Index int64
TimeStamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int64
}
Nonce 用來儲存一個隨機值,poW演算法中Nonce和區塊其他資訊一起進行Hash計算,使計算符合一定的條件,如比特幣的Hash值是需要前幾位為0,像000017504… 這樣我們需要找出這樣的一個隨機數使得Hash值前四位為0.
定義poW
type ProofOfWork struct {
block *Block
target *big.Int
}
func NewProofOfWork (b *Block) *ProofOfWork {
target:= big.NewInt(1)
target.Lsh(target,uint(256-targetBit))
pow:=&ProofOfWork{b,target}
return pow
}
target這裡使用了一個 大整數 ,將會用hash與target進行比較:先把雜湊轉換成一個大整數,然後檢測它是否小於目標。
在 NewProofOfWork 函式中,我們將 big.Int 初始化為 1,然後左移 256 - 1 位。則target(目標) 的 16 進位制形式為:
0x10000000000000000000000000000000000000000000000000000000000
準備資料
我們先使用IntToHex函式將int型資料轉行為16進位制。
func IntToHex(data int64) []byte {
buffer := new(bytes.Buffer) // 新建一個buffer
err := binary.Write(buffer, binary.BigEndian, data)
if nil != err {
log.Panicf("int to []byte failed! %v\n", err)
}
return buffer.Bytes()
}
上面說過,poW演算法中Nonce和區塊其他資訊一起進行Hash計算,使計算符合一定的條件。現在我們就用prepareData函式將Nonce與區塊其他資訊合併。
func (pow *ProofOfWork)prepareData(nonce int64)[]byte{
data:=bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Index),
IntToHex(pow.block.TimeStamp),
IntToHex(int64(targetBit)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
工作量證明
這段是實現poW的核心程式碼:
func (pow *ProofOfWork)Run()(int64 ,[]byte) {
var hashInt big.Int
var hash [32]byte
var nonce int64 =0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for {
dataBytes :=pow.prepareData(nonce) //獲取準備的資料
hash =sha256.Sum256(dataBytes) //對資料進行Hash
hashInt.SetBytes(hash[:])
fmt.Printf("hash: \r%x",hash)
if pow.target.Cmp(&hashInt) ==1 { //對比hash值
break
}
nonce++ //充當計數器,同時在迴圈結束後也是符合要求的值
}
fmt.Printf("\n碰撞次數: %d\n", nonce)
return int64(nonce),hash[:]
}
通過Run函式我們可以看到,函式不斷的迴圈查詢符合要求的nonce值。
當target值大於hashInt 退出迴圈,並返回計數次數和正確hash。
迴圈體內工作主要是:
準備塊資料
計算SHA-256值
轉成big int
與target比較
建立新的區塊
下面我將對對上篇文章中的NewBlock 函式進行適當的修改。
func NewBlock(index int64,data string ,prevBlockHash []byte )*Block{
block :=&Block{index,time.Now().Unix(),[]byte(data),prevBlockHash,[]byte{},0}
pow:= NewProofOfWork(block)
nonce ,hash :=pow.Run()
block.Hash =hash[:]
block.Nonce =nonce
return block
}
由於改造後新區塊的定義添加了Nonce ,所以我們要對Nonce也進行賦值,並且我們要通過poW演算法來重新生成區塊。
驗證區塊
本文在前面說過,區塊的資料有任意改動,哪怕是一個位元組的改動,Hash值都會隨著變化。因此在對於新生成的區塊,我們非常有必要對它重新計算hash,驗證是否合法。但由於本文程式碼僅是區塊鏈簡單演示,區塊驗證並非必要。
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
}
執行
func main() {
bc := NewBlockchain()
fmt.Printf("blockChain : %v\n", bc)
bc.AddBlock("Aimi send 100 BTC to Bob")
bc.AddBlock("Aimi send 100 BTC to Jay")
bc.AddBlock("Aimi send 100 BTC to Clown")
length := len(bc.blocks)
fmt.Printf("length of blocks : %d\n", length)
for i := 0; i < length; i++ {
pow :=NewProofOfWork(bc.blocks[i])
if pow.Validate() {
fmt.Println("—————————————————————————————————————————————————————")
fmt.Printf(" Block: %d\n",bc.blocks[i].Index)
fmt.Printf("Data: %s\n",bc.blocks[i].Data)
fmt.Printf("TimeStamp: %d\n",bc.blocks[i].TimeStamp)
fmt.Printf("Hash: %x\n",bc.blocks[i].Hash)
fmt.Printf("PrevHash: %x\n",bc.blocks[i].PrevBlockHash)
fmt.Printf("Nonce: %d\n",bc.blocks[i].Nonce)
}else {
fmt.Println("illegal block")
}
}
}