[譯]用go進行區塊鏈開發2:工作量證明

原地址https://github.com/XanthusL/blog-gen

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

簡介

上一篇文章我們根據區塊鏈的本質做了一個簡單的數據結構,並且實現了像它添加有連鎖關係的區塊:每一區塊連接着上一區塊。不過我們的區塊鏈實現存在嚴重問題:向鏈上添加區塊簡單又便宜。難於添加新區塊是區塊鏈和比特幣的一個重要特點。
今天我們來解決這個問題

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

如果要往區塊鏈中存放數據,必須執行一些高難度的工作,這是區塊鏈的一個核心觀點。正是這種高難度工作使區塊鏈安全一致。另外,這些高難度工作會得到獎勵(這就是挖礦獲取比特幣的原理)。

這個機制跟現實生活中的很像:一個人必須努力工作獲得報酬以維持生計。區塊鏈中,一些想通過添加新區塊以獲得報酬的參與者(礦工)會維持網絡。他們的工作結果就是,一個區塊以不影響整個區塊鏈數據庫穩定性的安全方式被合併到區塊鏈中。值得注意的是,完成這項工作的人需要爲之證明。

這個“努力工作並證明”的機制被稱爲工作量證明。它難就難在需要超強的運算能力:就算是高性能計算機也無法快速完成。再者,爲了保持每小時6個新區塊的產生速度這項工作的難度會時不時地增加。比特幣中,這項工作的目標是找到滿足一些要求的區塊的哈希值。這個哈希值就是工作量的證明。因此,尋找一個證明就是實際上的工作。

最後一點需要注意的是,工作量證明算法必須符合一個要求:工作難度大,但驗證簡單。一個證明通常會交給其他人,因此對他們來說不宜花太多時間來驗證。

哈希

我們將在這一部分討論哈希。如果你熟悉這個概念,可以跳過這部分。

哈希是爲特定數據取哈希值的過程。一個哈希值是用於計算它的數據的唯一表現。哈希函數是用任意長度的數據生成指定長度的哈希值。一下是哈希的部分特性:
1. 通過哈希值不能得到原始數據。因此,哈希不是加密
2. 特定的數據只能有一個哈希值並且哈希值是唯一的
3. 改變數據的任意一個字節,它的哈希值就截然不同

"I like donutes"
    ↓
SHA256(...)
    ↓
f80867f6efd4484c23b0e7184e53fe4af6ab49b97f5293fcd50d5b2bfa73a4d0

哈希函數廣泛應用於數據一致性校驗。一些軟件在軟件包中提供發佈版本的校驗值。下載好一個文件後就可以用哈希函數取它的哈希值並和軟件開發者提供的哈希值進行比對。

在區塊鏈中,哈希用來保證區塊的一致性。哈希算法的輸入數據包含了上一區塊的哈希值,因此不可能(或者說難度極大)篡改鏈中的區塊:改變區塊就要重新計算它的以及它後面的所有區塊的哈希值。

哈希現金(Hashcash

比特幣用了哈希現金,這是一個最初用來阻止垃圾郵件的工作量證明算法。可以把它分爲一下幾步:

  1. 獲取一部分公開數據(在電子郵件中,是收件人的地址;在比特幣中,是區塊的頭)
  2. 爲它添加一個計數器。計數器從0開始計數
  3. 數據+計數器的組合體取哈希值
  4. 檢查哈希值是否滿足條件
    1. 滿足,完成了。
    2. 不滿足,計數器計數加一,重複第3步和第4步.

因此這是一個暴力的算法:改變計數,計算新的哈希值,檢查,增加計數,計算哈希等等。這便是它計算昂貴的原因。

現在我們仔細看一下哈希需要滿足的條件。在原版哈希現金實現中的條件聽起來像“哈希值前20個位(bit)必須是0”。比特幣中,這個需求會時不時地調整,因爲在設計上,不管運算能力如何提升、越來越多的礦工加入,必須每10分鐘產生一個區塊

爲了演示這個算法,我用上一個例子中的數據(“I like donuts”)發現了一個開頭3個字節爲0的哈希值:

"I like donutesca07ca"
    ↓
SHA256(...)
    ↓
0000002f7c1fe31cb82acdc082cfec47620b7e4ab94f2bf9e096c436fc8cee06    

ca07ca是計數器值的十六進制表示,在十進制中是13240266

實現

那麼,我們已經完成了理論部分,現在開始寫代碼。首先定義挖礦難度:

const targetBits = 24

在比特幣中,“目標位”是存放區塊產生時挖掘難度的區塊頭。我們暫時不實現目標調整算法,因此把挖礦難度定義爲一個共用的常量。

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結構體。“目標”是上一部分中說到的需要滿足的要求的另一個名字。我們用了一個整型,因爲要和目標對比哈希:把一個哈希值轉換成大整型並檢查它是否比目標小。

NewProofOfWork函數中,我們用數值1初始化一個big.Int並把它左移256 - targetBits位。256是SHA-256哈希值所佔的位數,而且我們打算用的哈希算法就是SHA-256。目標的十六進制表示是:

0x10000000000000000000000000000000000000000000000000000000000

它在內存中佔29個字節。這是與之前例子中哈希值的直觀對照:

0fac49161af82ed938add1d8725835cc123a1a87b1b196488360e58d4bfb51e3
0000010000000000000000000000000000000000000000000000000000000000
0000008b0f41ec78bab747864db66bcb9fb89920ee75f43fdaaeb5544f7f76ca

第一個哈希值(用“I like donuts”算出)比目標要大,因此它不是有效的工作量證明。第二個哈希值(用“I like 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
}

這段代碼很簡單:我們只是把區塊的字段和目標、當前計數合併到一塊。nonce就是上面介紹的哈希現金中的計數器,它是一個密碼學術語。

好了,一切就緒,接下來我們實現工作量證明算法的核心部分:

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是哈希值的整數表示;nonce是計數器。接下來,跑一個“無限”循環:它受maxNonce限制,即mathMaxInt64;這是爲了避免nonce溢出。儘管我們的PoW實現對nonce溢出來說難度太小了,不過最好加上這個檢查,以防萬一。

循環體中做了這些:
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的屬性保存了下來。驗證一個證明的時候要用到nonce,因此這是有必要的。現在的Block結構體是這樣的:

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
}

這就是我們要用之前保存的nonce的地方。

再次檢查是否一切正常:

func main() {
    ...

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

輸出:

...

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

結語

我們的區塊鏈離實際的架構更近了一步:現在添加區塊需要困難的工作,因而可以挖礦。但它還缺少一些關鍵特性:數據庫不是持久化的,沒有錢包、地址、交易,沒有共識機制。這些特性我們將在後續文章中實現,目前的話,愉快地挖礦吧!



鏈接:

  1. 完整源碼
  2. 區塊鏈哈希算法
  3. 工作量證明
  4. 哈希現金
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章