1. 程式人生 > >用go編寫區塊鏈系列之2---PoW

用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