1. 程式人生 > >用 golang 實現區塊鏈系列二 | 工作量證明

用 golang 實現區塊鏈系列二 | 工作量證明

介紹

上篇文章中, 我們構建了一個很簡單的資料結構,這個結構就是區塊鏈資料庫的本質。而且我們賦予了它們類似於鏈式操作中新增資料塊的能力:每個區塊和前一個區塊相連結。不過哦,我們的區塊鏈實現有一個很大的瑕疵:新增一個區塊太簡單了,成本太低了。區塊鏈和比特幣的其中一個重要基石則是新增新的區塊非常困難。今天,我們來修復這個瑕疵。

工作量證明

區塊鏈的一個關鍵思想就是在新增資料到區塊鏈之前需要做一些很困難的工作。這個困難的工作是的區塊鏈安全和一致。同時,這個繁重的工作也是有報償的(這就是人們如何通過挖礦獲得幣)。

這個機制和現實生活非常像:一是不得不努力工作去獲得報酬,以此來維持生活。在區塊鏈中,一些網路的參與者(礦工)工作,並維持網路,新增新的區塊到網路中,並從這份工作中獲得相應的報酬。他們工作的結果就是以安全的方式將區塊納入到網路中,這是一種維持整個區塊鏈資料庫穩定的方式。值得注意的是,完成這項工作的人必須要去證明這一點。

整個 幹活並證明 的機制被稱為 工作量證明。這很難,因為它需要大量的計算能力:甚至是高效能的電腦也難以快速算出。除此之外,這個工作困難的點還在於隨著時間的增加,新的區塊被保持在每小時 6 個的速率。在比特幣中,這個工作的目標是為區塊找一個有一些限制的 hash 值。這個 hash 就是證據。所以,找到證據是真實的工作。

還有最後一個點。工作量證明的演算法必須有一些要求:很難算,但是驗證很簡單。一份證據通常被其他人拿著,所以,驗證不應該花費很長時間。

計算雜湊(Hashing)

這這一段中,我們將討論雜湊計算。如果你對於這個概念很清楚,當然可以跳過這個部分。

Hashing 是一個針對特定資料計算雜湊的過程。一個雜湊是資料被計算出的獨一無二的代表。雜湊函式是獲取不定長度的資料併產生固定長度的雜湊值的過程。雜湊計算有一些關鍵功能:

  1. 源資料不能被通過雜湊值恢復。這樣,雜湊就不是編碼了。
  2. 確定的資料有且只能有一個雜湊值
  3. 即使只改變輸入資料的一個位元組也會產生差異極大的雜湊值

hashing example

雜湊演算法被廣泛地應用於計算資料一致性。一些軟體附帶公佈出軟體的 checksum。在下載之後,你可以把它餵給雜湊函式並將產生的雜湊值和軟體開發者提供的雜湊值對比。

在區塊鏈中,雜湊被用來保護區塊的資料一致性。一個雜湊演算法的輸入值包含著上個區塊的 hash 值。這就使得修改鏈上的區塊變得不可能(至少是難度極高):不僅要重新計算它自己的 hash,還得算出它之後的所有 hash。

Hashcash

比特幣使用 Hashcash,一個最初被髮展出來用於遮蔽垃圾郵件的工作量證明的演算法。它可以被分解為下面幾步:

  1. 獲取一些公眾知道的資料(在郵件的場景下,它是郵箱地址,在比特幣場景下,它是區塊頭)。
  2. 給它加個計數器,計數器從 0 開始算。
  3. 獲取資料和計數器混合的雜湊值。
  4. 確認這個雜湊值滿足了一些需求。如果滿足了就完成,否則增加計數器並重復步驟 3 和步驟 4.

這是一個很粗暴的演算法:改變計數器,算新的雜湊,確認,增加計數器,計算雜湊,周而復始。這就是為何算力如此高昂的原因。

現在,我們來接觸一些滿足雜湊值得必要條件。原始的 Hashcash 實現的演算法,必要條件就像是 “雜湊的頭 20 個位元必須得是 0”。在比特幣中,這個滿足條件隨著時間的推移不斷調整。因為在設計上,區塊必須要每 10 分鐘生成一個,儘管隨著時間不斷地增加,算力也增加了,網路上也越來越多的礦工入場也必須如此。

為了演示這個演算法,我拿了上個例子中的資料(“I like donuts”)並找一個開頭 3 個 0 位元的雜湊值。

hashcash example

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

實現

好了,學術部分說完了,來開始寫程式碼吧!首先,我們定義一個挖坑困難度:

const targetBits = 24

在比特幣種,“目標位元”是在每個被挖出來的區塊頭上儲存的困難度。我們暫時並不會實現這個動態調整目標值的演算法,所以,我們可以以全域性常量的形式定義這個困難度。

24 是一個隔斷值,我們的目標是在記憶體中找到一個少於 256 位元的目標值,而且我們想要使它有足夠的不同,還不能太大。因為差別越大,越難找到合適的 hash 值

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 的結構,它有區塊指標和一個目標指標。 “target(目標)”是上一段中條件描述的另一個名字。我們使用一個 big 整型,因為這是我們對比雜湊和目標值的方式:我們轉換雜湊成一個 big integer,然後和目標值對比是不是比它小。

在 NewProofOfWork 函式中,我們用 1 初始化了一個 big.int ,並且獲取它 ** 256 - targetBits ** 左邊的值。256 是 SHA-256 雜湊演算法的位元長度,並且這個 SHA-256 雜湊演算法就是我們要用的,target 的十六進位制表示是:

0x10000000000000000000000000000000000000000000000000000000000

它在記憶體中生成了 29 位元。這裡是上個例子中生成的雜湊的視覺化對比:

第一個雜湊(“I like donuts” 算出來的)要比目標值大,所以這並不是一個通過校驗的工作證明。第二個雜湊(“I like donutsca07ca”算出來的)要比目標值藥效,所以它是一個合理的證明。

你可以想象一下目標值在區間的上邊界:如果一個數字(雜湊)比邊界要小,它就是一個合理的。反之亦然。較低的邊界會產生更少的合理的值,所以越困難的工作就需要去找到一個有效的值

現在,我們需要資料區計算 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
}

這一塊很簡單:我們只是合併區塊的值帶著目標值和 nonce。 這裡的 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. 轉化 hash 成一個 big integer.
  4. 和目標值比較

就像我們之前所解釋的那樣簡單。現在我們可以刪掉 Block 裡的 SetHash 函數了,然後順便改造一下 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

你現在可以看到每個雜湊值都以三個 bytes 為 0 開頭,而且它也花了一些時間去獲取雜湊值。

這裡還有一件事要去做。我們得讓驗證工作量變得可行。

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 的原因。

來再確認一下都沒問題了:

unc 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

最後

我們的區塊鏈離真實的架構更近了一步:現在新增區塊需要困難的工作了,這就讓挖坑成為了可能。但仍舊缺少一些至關重要的功能:區塊鏈資料庫並未持久化,也沒有錢包,地址,交易,也沒有一致化機制。所有的這些我們都要在未來的文章中實現,現在,祝挖礦快樂~