1. 程式人生 > >區塊鏈poW 工作量證明

區塊鏈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")
		}
	}
}