用go編寫區塊鏈系列之2---PoW
在前一篇文章中,我們構建了一個最簡單的額區塊鏈,本文中我們將改進這個區塊鏈,加入PoW機制,使得這個區塊鏈變成一個可以挖礦的區塊鏈。原文見https://jeiwan.cc/posts/building-blockchain-in-go-part-2/
1 Proof-of-Work(PoW)
區塊鏈的PoW機制的思想就是,一個節點必須做艱難的工作才能夠推送資料到區塊鏈中。這個艱難工作保證了區塊鏈的安全和一致性。同時,區塊鏈也會為這個工作提供獎勵。這有點像我們的人生,一個人必須努力工作才能獲得他想要的額生活。
2 Hash演算法
Hash演算法就是為特定的資料計算其Hash值的過程。一個hash函式可以輸入任意長度的資料,但是其輸出結果總是固定長度的hash值。Hash演算法的特徵有:
- 不能根據Hash值推導原始資料,所以Hash演算法並不是一種加密演算法。
- 特定資料只會對應一個唯一特定的hash值。
- 輸入資料即使只改變一個數據位,hash結果也會產生很大的變化。
Hash演算法被廣泛的用於資料的一致性檢查。在區塊鏈中,hash演算法被用於驗證區塊的一致性。hash演算法的輸入資料包含上一個區塊的hash值,這使得修改區塊鏈資料成為不可能的任務:一個人必須計算這個區塊的hash,以及其後所有區塊的hash。
3 Hashcash
比特幣使用Hashcash,一種最早設計用來攔截垃圾郵件的演算法。Hashcash演算法包含以下幾步:
(1)準備一些已知的公共資料data,比如區塊頭。
(2)給這些資料額外新增一個計數器counter,counter從0開始遞增。
(3)計算data+counter拼接後字串的Hash值
(4)檢查計算出的Hash值是否滿足要求,如果滿足,則工作完成;如果不滿足,則counter增1,重新執行(3)、(4)步驟。
在最開始的Hashcash演算法中,對Hash值的檢查要求是Hash值的最開始的20位全為0。在比特幣中,這個要求會隨時調整以使得計算難度滿足沒10分鐘生產一個區塊。
為了驗證這個演算法,以字串“I like donuts”為例,要找到一個counter使得I like donuts N的Hash值前面3個位元組都為0,
找到的counter將是ca07ca,換算成10進位制是13240266。
4 PoW演算法實現
首先我們定義挖礦難度係數為:
const targetBits = 24
雜湊函式SHA256()計算出的hash值是256位,我們希望前面24位都是0。
然後我們定義一個挖礦結構體:
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,block中儲存了要挖出區塊的資料,target中儲存了跟挖礦難度相對應的目標值。
在NewProofOfWork
函式中,我們先將target賦值為1,然後再左移256-targetBits位。如果一個Hash值小於這個target,則說明它的前面24位都是0。比如我們現在構造的target等於:
0x10000000000000000000000000000000000000000000000000000000000
它在記憶體中佔用了29個位元組,對於下面3個數:
0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca
第一個數大於target,它不滿足要求;第3個數小於target,我們可以看到它前面24位都為0。
現在我們需要構造給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
}
在這裡我們準備計算Hash值的資料為block.PrevBlockHash+block.Data+block.Timestamp+target+nonce拼接而成的資料,其中nonce就是上面的計數器counter。
所有資料準備完畢,現在開始實現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[:]
}
我們在for迴圈裡面做的事情:
(1)準備資料
(2)計算Hash值
(3)將Hash值轉成一個整數
(4)比較整數和目標值的大小
(5)結束或遞增Nonce
為了配合挖礦演算法,我們需要更改塊結構:
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
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
}
最後貼上完整程式碼:
package main
import (
"strconv"
"bytes"
"crypto/sha256"
"time"
"fmt"
"math/big"
"encoding/binary"
"log"
"math"
)
const targetBits = 24
var maxNonce = math.MaxInt64
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
type ProofOfWork struct {
block *Block
target *big.Int
}
func (b *Block) SetHash() {
timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
hash := sha256.Sum256(headers)
b.Hash = hash[:]
}
func NewProofOfWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))
pow := &ProofOfWork{b, target}
return pow
}
func IntToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
log.Panic(err)
}
return buff.Bytes()
}
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
}
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.Println()
fmt.Println(nonce)
return nonce, 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 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
}
type Blockchain struct {
blocks []*Block
}
func (bc *Blockchain) AddBlock(data string) {
prevBlock := bc.blocks[len(bc.blocks)-1]
newBlock := NewBlock(data, prevBlock.Hash)
bc.blocks = append(bc.blocks, newBlock)
}
func NewGenesisBlock() *Block {
return NewBlock("Genesis Block", []byte{})
}
func NewBlockchain() *Blockchain {
return &Blockchain{[]*Block{NewGenesisBlock()}}
}
func main() {
bc := NewBlockchain()
bc.AddBlock("Send 1 BTC to Ivan")
bc.AddBlock("Send 2 more BTC to Ivan")
for _, block := range bc.blocks {
fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
fmt.Println()
}
}
執行結果:
Mining the block containing "Genesis Block"
00000040831b867e4a7e75460eb76f9a3b32c5be4043467d11d09240522d25d9
7470848
Mining the block containing "Send 1 BTC to Ivan"
000000f49ad3dbc76bce70f317082dbbf1eac00f044e8807865bba35a30d2fba
32065795
Mining the block containing "Send 2 more BTC to Ivan"
0000002250ba1c3254ad765b4a0a47af2e5452c9bb27102cf5e0fe3f5de10070
29669735
Prev. hash:
Data: Genesis Block
Hash: 00000040831b867e4a7e75460eb76f9a3b32c5be4043467d11d09240522d25d9
Prev. hash: 00000040831b867e4a7e75460eb76f9a3b32c5be4043467d11d09240522d25d9
Data: Send 1 BTC to Ivan
Hash: 000000f49ad3dbc76bce70f317082dbbf1eac00f044e8807865bba35a30d2fba
Prev. hash: 000000f49ad3dbc76bce70f317082dbbf1eac00f044e8807865bba35a30d2fba
Data: Send 2 more BTC to Ivan
Hash: 0000002250ba1c3254ad765b4a0a47af2e5452c9bb27102cf5e0fe3f5de10070
Process finished with exit code 0