使用Go構建區塊鏈 第2部分:工作量證明

銀河1號發表於2019-01-23

Introduction

在上一篇文章中,我們構建了一個非常簡單的資料結構,這是區塊鏈資料庫的本質。我們可以通過它們之間的鏈狀關係為它新增區塊:每個區塊都連結到前一個塊。我們的區塊鏈實現有一個重大缺陷:向鏈中新增區塊很容易。區塊鏈和比特幣的核心之一是:新增新區塊是一項艱苦的工作。今天我們要解決這個缺陷。

Proof-of-Work(工作量證明)

區塊鏈的一個關鍵思想是,必須進行一些艱苦的工作才能將資料放入其中。正是這項艱苦的工作使區塊鏈變得安全和一致。此外,這項艱苦的工作也得到了回報(這也就是通過挖礦獲得幣)。

這種機制與現實生活中的機制非常相似:人們必須努力工作,才能獲得獎勵並維持生命。在區塊鏈中,網路的一些參與者(礦工)努力維持網路,向其新增新區塊,並獲得對其工作的獎勵。由於他們的工作,區塊以安全的方式被整合到區塊鏈中,這保持了整個區塊鏈資料庫的穩定性。值得注意的是,完成工作的人必須證明這一點。

這整個“努力工作和證明”的機制被稱為工作量證明。這很難,因為它需要大量的計算能力:即使是高效能的計算機也無法快速完成。另外,這個工作的困難度會隨著時間不斷增長,以保持每 10 分鐘出 1 個新塊的速度。在比特幣中,這種工作的目標是找到一個塊的雜湊,滿足一些要求。這個雜湊,也就充當了證明的角色。因此,尋求證明(尋找有效雜湊),就是礦工實際要做的事情。

最後要注意的一點是。工作量證明演算法必須滿足要求:完成工作很難,但驗證證明很容易。證明通常會交給其他人,因此對他們而言,驗證它不應該花費太多時間。

Hashing

在本段中,我們將討論雜湊。如果您熟悉這個概念,可以跳過這一部分。

雜湊是獲取指定資料的雜湊的過程。雜湊是計算資料的唯一表示。雜湊函式是一種獲取任意大小資料並生成固定大小雜湊的函式。以下是雜湊的一些主要功能:

  1. 無法從雜湊中恢復原始資料。因此,雜湊不是加密。
  2. 某些資料只能有一個雜湊值,雜湊值是唯一的。
  3. 改變輸入資料中的一個位元組將導致完全不同的雜湊。

使用Go構建區塊鏈  第2部分:工作量證明

雜湊函式廣泛用於檢查資料的一致性。除軟體包外,某些軟體提供商還會發布校驗和。下載檔案後,您可以將其提供給雜湊函式,並將生成的雜湊與軟體開發人員提供的雜湊進行比較。

在區塊鏈中,雜湊用於保證區塊的一致性。雜湊演算法的輸入資料包含前一個區塊的雜湊,因此無法(或者至少非常困難)修改鏈中的區塊:必須重新計算其雜湊和其後所有區塊的雜湊值。

Hashcash

比特幣使用 Hashcash,一種最初開發用於防止電子郵件垃圾郵件的工作量證明演算法。它可以分為以下幾個步驟:

  1. 拿一些公開的資料(如果是電子郵件,它是接收者的電子郵件地址;對於比特幣,它是塊頭)。
  2. 新增一個計數器。計數器從0開始。
  3. data(資料)counter(計數器) 組合到一起,獲得一個雜湊
  4. 檢查雜湊符合某些要求。
    1. 如果確實如此,那就完成了。
    2. 如果沒有,請增加計數器並重復步驟3和4。

因此,這是一個暴力演算法:你改變計數器,計算一個新的雜湊,檢查它,增加計數器,計算一個雜湊等。這就是為什麼它的計算成本很高。

現在讓我們仔細看看雜湊必須滿足的要求。在最初的Hashcash實現中,它的要求是 “一個雜湊的前 20 位必須是 0”。在比特幣中,需求會不時調整,因為,儘管計算能力隨著時間的推移而增加,並且越來越多的礦工加入網路,但必須保證每 10 分鐘生成一個塊。

為了演示這個演算法,我從前面的例子中獲取了資料(“我喜歡甜甜圈”)並找到了一個以3個零位元組開頭的雜湊:

使用Go構建區塊鏈  第2部分:工作量證明

ca07ca是計數器的十六進位制值,十進位制系統中為13240266。

Implementation

好的,我們已經完成了理論,讓我們編寫程式碼!首先,讓我們來定義挖掘的難度:

const targetBits = 24複製程式碼

在比特幣中,當一個塊被挖出來以後,“target bits” 代表了區塊頭裡儲存的難度,也就是開頭有多少個 0。目前我們不會實現目標調整演算法,因此我們可以將難度定義為全域性常量。

24是一個任意數字,我們的目標是讓一個目標在記憶體中佔用少於256位。我們希望差異足夠大,但不要太大,因為差異越大,找到合適的雜湊越困難。

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)的指標。這裡的 “目標” ,也就是前一節中所描述的必要條件。這裡使用了一個 大整數 ,我們會將雜湊與目標進行比較:先把雜湊轉換成一個大整數,然後檢測它是否小於目標。

NewProofOfWork 函式中,我們將 big.Int 初始化為 1,然後左移 256 - targetBits 位。256 是一個 SHA-256 雜湊的位數,我們將要使用的是 SHA-256 雜湊演算法。target(目標) 的 16 進位制形式為:

0x10000000000000000000000000000000000000000000000000000000000複製程式碼

它在記憶體中佔用29個位元組。這裡是與前面例子中的雜湊的視覺比較:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca複製程式碼

第一個雜湊值(根據“我喜歡甜甜圈”計算)比目標大,因此它不是有效的工作證明。第二個雜湊(計算在“我喜歡donutsca07ca”)小於目標,因此它是一個有效的證據。

您可以將目標視為範圍的上邊界:如果數字(雜湊)低於邊界,則它是有效的,反之亦然。降低邊界將導致有效數字減少,因此找到有效數字所需的工作更加困難。

現在,我們需要資料進行雜湊處理。我們準備吧:

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
}複製程式碼

這個部分很簡單:我們只是將 target ,nonce 與 Block 進行合併。nonce這裡是上面Hashcash描述的計數器,這是一個加密術語。

好的,所有準備工作都已完成,讓我們實現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; nonce是計數器。接下來,我們執行一個“無限”迴圈:它受限於maxNonce, 它等於 math.MaxInt64;這樣做是為了避免nonce的溢位。雖然我們的PoW實現的難度太低而不能使計數器溢位,但為了以防萬一,進行此檢查仍然更好。

在這個迴圈中,我們做的事情有:

  1. 準備資料
  2. 用 SHA-256 對資料進行雜湊
  3. 將雜湊轉換成一個大整數
  4. 將這個大整數與目標進行比較

跟之前所講的一樣簡單。現在我們可以移除 BlockSetHash 方法,然後修改 NewBlock 函式:

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 的一個屬性。這是十分有必要的,因為待會兒我們對這個工作量進行驗證時會用到 nonceBlock 結構現在看起來像是這樣:

type Block struct {
	Timestamp     int64
	Data          []byte
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}
複製程式碼

好的!讓我們執行程式,看看一切是否正常:

Mining the block containing "Genesis Block"
00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Mining the block containing "Send 1 BTC to Ivan"
00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Mining the block containing "Send 2 more BTC to Ivan"
000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe

Prev. hash:
Data: Genesis Block
Hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1

Prev. hash: 00000041662c5fc2883535dc19ba8a33ac993b535da9899e593ff98e1eda56a1
Data: Send 1 BTC to Ivan
Hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804

Prev. hash: 00000077a856e697c69833d9effb6bdad54c730a98d674f73c0b30020cc82804
Data: Send 2 more BTC to Ivan
Hash: 000000b33185e927c9a989cc7d5aaaed739c56dad9fd9361dea558b9bfaf5fbe複製程式碼

好極了!您可以看到每個雜湊現在以三個零位元組開始,並且需要一些時間來獲取這些雜湊值。

還有一件事要做:對工作量證明進行驗證。

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() {
	...

	for _, block := range bc.blocks {
		...
		pow := NewProofOfWork(block)
		fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
		fmt.Println()
	}
}
複製程式碼

Output:

...

Prev. hash:
Data: Genesis Block
Hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
PoW: true

Prev. hash: 00000093253acb814afb942e652a84a8f245069a67b5eaa709df8ac612075038
Data: Send 1 BTC to Ivan
Hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
PoW: true

Prev. hash: 0000003eeb3743ee42020e4a15262fd110a72823d804ce8e49643b5fd9d1062b
Data: Send 2 more BTC to Ivan
Hash: 000000e42afddf57a3daa11b43b2e0923f23e894f96d1f24bfd9b8d2d494c57a
PoW: true
複製程式碼

Conclusion

我們的區塊鏈更接近其實際架構:現在新增區塊需要努力工作,因此可以進行挖掘。但它仍然缺乏一些關鍵特徵:區塊鏈資料庫不是持久的,沒有錢包,地址,交易,也沒有共識機制。所有這些我們將在未來的文章中實現,現在,快樂的採礦吧!


英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-2/


更多文章歡迎訪問 http://www.apexyun.com/

聯絡郵箱:public@space-explore.com

(未經同意,請勿轉載)


相關文章