[譯] 用 Go 編寫你本身的區塊鏈挖礦算法!

若是你對下面的教程有任何問題或者建議,加入咱們的 Telegram 消息羣,能夠問咱們全部你想問的!前端

隨着最近比特幣和以太坊挖礦大火,很容易讓人好奇,這麼大驚小怪是爲何。對於加入這個領域的新人,他們會聽到一些瘋狂的故事:人們用 GPU 填滿倉庫,每月賺取價值數百萬美圓的加密貨幣。電子貨幣挖礦究竟是什麼?它是如何運做的?我如何能試着編寫本身的挖礦算法?python

在這篇博客中,咱們將會帶你解答上述每個問題,並最終完成一篇教你如何編寫本身的挖礦算法的教程。咱們將展現給你的算法叫作工做量證實,它是比特幣和以太坊這兩個最流行的電子貨幣的基礎。別急,咱們立刻將爲你解釋它是如何運做的。android

什麼是電子貨幣挖礦ios

爲了有價值,電子貨幣須要有必定的稀缺性。若是誰均可以隨時生產出他們想要的任意多的比特幣,那麼做爲貨幣,比特幣就毫無價值了。(等一下,美國聯邦儲備不是這麼作了麼?打臉)比特幣算法每十分鐘將會發放一些比特幣給網絡中一個獲勝成員,這樣最多能夠供給大約 122 年。因爲定量的供應並非在最一開始就所有發行,這種發行時間表在必定程度上也控制了膨脹。隨着時間流逝,發行地速度將愈來愈慢。git

決定勝者是誰並給出比特幣的過程須要他完成必定的「工做」,並與同時也在作這個工做的人競爭。這個過程就叫作挖礦,由於它很像採礦工人花費時間完成工做並最終(但願)找到黃金。github

比特幣算法要求參與者,或者說節點,完成工做並相互競爭,來保證比特幣不會發行過快。web

挖礦是如何運做的?算法

一次谷歌快速搜索「比特幣挖礦如何運做?」將會給你不少頁的答案,解釋說比特幣挖礦要求節點(你或者說你的電腦)解決一個很難的數學問題。雖然從技術上來講這是對的,可是簡單的把它稱爲一個「數學」問題太過有失實質而且太過陳腐。瞭解挖礦運做的內在原理是很是有趣的。爲了學習挖礦運做原理,咱們首先要了解一下加密算法和哈希。編程

哈希加密的簡短介紹json

單向加密的輸入值是能讀懂的明文,像是「Hello world」,並施加一個函數在它上面(也就是,數學問題)生成一個不可讀的輸出。這些函數(或者說算法)的性質和複雜度各有不一樣。算法越複雜,逆運算解密就越困難。所以,加密算法能有力保護像用戶密碼和軍事代碼這類事物。

讓咱們來看一個 SHA-256 的例子,它是一個很流行的加密算法。這個哈希網站能讓你輕鬆計算 SHA-256 哈希值。讓咱們對「Hello world」作哈希運算來看看將會獲得什麼:

試試對「Hello world」重複作哈希運算。你每次都將會獲得一樣的哈希值。給定一個程序相同的輸入,反覆計算將會獲得相同的結果,這叫作冪等性。

加密算法一個基本的屬性就是(輸出值)靠逆運算很難推算輸入值,可是(靠輸入值)很容易就能驗證輸出值。例如,用上述的 SHA-256 哈希算法,其餘人將 SHA-256 哈希算法應用於「Hello world」很容易的就能驗證它確實輸出同一個哈希值結果,可是想從這個哈希值結果推算出「Hello world」將會很是困難。這就是爲何這類算法被稱爲單向

比特幣採用 雙 SHA-256 算法,這個算法就是簡單的將 SHA-256 再一次應用於「Hello world」的 SHA-256 哈希值。在這篇教程中,咱們將只應用 SHA-256 算法。

挖礦

如今咱們知道了加密算法是什麼了,咱們能夠回到加密貨幣挖礦的問題上。比特幣須要找到某種方法,讓但願獲得比特幣參與者「工做」,這樣比特幣就不會發行的過快。比特幣的實現方式是:讓參與者不停地作包含數字和字母的哈希運算,直到找到那個以特定位數的「0」開頭的哈希結果。

例如,回到哈希網站而後對「886」作哈希運算。它將生成一個前綴包含三個零的哈希值。

可是,咱們怎麼知道 「886」 能得出一個開頭三個零的結果呢?這就是關鍵點了。在寫這篇博客以前,咱們不知道。理論上,咱們須要遍歷全部數字和字母的組合、測試結果,直到獲得一個可以匹配咱們需求的三個零開頭的結果。給你舉一個簡單的例子,咱們其實已經預先作了計算,發現 「886」 的哈希值是三個零開頭的。

任何人均可以很輕鬆的驗證 「886」 的哈希結果是三個零前綴,這個事實證實了:我作了大量的工做來對不少字母和數字的組合進行測試和檢查以得到這個結果。因此,若是我是第一個獲得這個結果的人,我就能經過證實我作了工做來獲得比特幣 - 證據就是任何人都能輕鬆驗證 「886」 的哈希結果爲三零前綴,正如我宣稱的那樣。這就是爲何比特幣共識算法被稱爲工做量證實

可是若是我很幸運,我第一次嘗試就獲得了三零前綴的結果呢?這幾乎是不可能的,而且那些偶然狀況下第一次就成功挖到了區塊(證實他們作了工做)的節點會被那些作了額外工做來找到合適的哈希值的成千上萬的其餘區塊所壓倒。試試看,在計算哈希的網站上輸入任意其餘的字母和數字的組合。我打賭你不會獲得一個三零開頭的結果。

比特幣的需求要比這個複雜不少(更多個零的前綴!),而且可以經過動態調節需求來確保工做不會太難也不會太容易。記住,目標是每十分鐘發行一次比特幣,因此若是太多人在挖礦,就須要將工做量證實調整的更難完成。這就叫難度調節(adjusting the difficulty)。爲了達成咱們的目的,難度調整就意味着需求更多的零前綴。

如今你就知道了,比特幣共識機制比單純的「解決一個數學問題」要有意思的多!

足夠多背景介紹了。咱們開始編程吧!

如今咱們已經有了足夠多的背景知識,讓咱們用工做量共識算法來創建本身的比特幣程序。咱們將會用 Go 語言來寫,由於咱們在 Coral Health 中使用它,而且說實話,棒極了

開始下一步以前,我建議讀者讀一下咱們以前的博文,Code your own blockchain in less than 200 lines of Go!並非硬性需求,可是下面的例子中咱們將講的比較粗略。若是你須要更多細節,能夠參考以前的博客。若是你對前面這篇很熟悉了,直接跳到下面的「工做量證實」章節。

結構

咱們將有一個 Go 服務,咱們就簡單的把全部代碼就放在一個 main.go 文件中。這個文件將會提供給咱們所需的全部的區塊鏈邏輯(包括工做量證實算法),幷包括全部 REST 接口的處理函數。區塊鏈數據是不可改的,咱們只須要 GETPOST 請求。咱們將用瀏覽器發送 GET 請求來觀察數據,並使用 Postman 來發送 POST 請求給新區塊(curl 也一樣好用)。

引包

咱們從標準的引入操做開始。確保使用 go get 來獲取以下的包

github.com/davecgh/go-spew/spew 在終端漂亮地打印出你的區塊鏈

github.com/gorilla/mux 一個使用方便的層,用來鏈接你的 web 服務

github.com/joho/godotenv 在根目錄的 .env 文件中讀取你的環境變量

讓咱們在根目錄下建立一個 .env 文件,它僅包含一個咱們一下子將會用到的環境變量。在 .env 文件中寫一行:ADDR=8080

對包做出聲明,並在根目錄的 main.go 定義引入:

package main

import (
        "crypto/sha256"
        "encoding/hex"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "os"
        "strconv"
        "strings"
        "sync"
        "time"

        "github.com/davecgh/go-spew/spew"
        "github.com/gorilla/mux"
        "github.com/joho/godotenv"
)
複製代碼

若是你讀了在此以前的文章,你應該記得這個圖。區塊鏈中的區塊能夠經過比較區塊的 previous hash 屬性值和前一個區塊的哈希值來被驗證。這就是區塊鏈保護自身完整性的方式以及黑客組織沒法修改區塊鏈歷史記錄的緣由。

BPM 是你的心率,也就是一分鐘心跳次數。咱們將會用一分鐘內你的心跳次數做爲咱們放到區塊鏈中的數據。把兩個手指放到手腕數一數一分鐘脈搏內跳動的次數,記住這個數字。

一些基礎探測

讓咱們來添加一些在引入後將會須要的數據模型和其餘變量到 main.go 文件

const difficulty = 1

type Block struct {
        Index      int
        Timestamp  string
        BPM        int
        Hash       string
        PrevHash   string
        Difficulty int
        Nonce      string
}

var Blockchain []Block

type Message struct {
        BPM int
}

var mutex = &sync.Mutex{}
複製代碼

difficulty 是一個常數,定義了咱們但願哈希結果的零前綴數目。須要獲得越多的零,找到正確的哈希輸入就越難。咱們就從一個零開始。

Block 是每個區塊的數據模型。別擔憂不懂 Nonce,咱們稍後會解釋。

Blockchain 是一系列的 Block,表示完整的鏈。

Message 是咱們在 REST 接口用 POST 請求傳送進來的、用以生成一個新的 Block 的信息。

咱們聲明一個稍後將會用到的 mutex 來防止數據競爭,保證在同一個時間點不會產生多個區塊。

Web 服務

讓咱們快速鏈接好網絡服務。建立一個 run 函數,稍後在 main 中調用他來支撐服務。還須要在 makeMuxRouter() 中聲明路由處理函數。記住,咱們只須要用 GET 方法來追溯區塊鏈內容, POST 方法來建立區塊。區塊鏈不可修改,因此咱們不須要修改和刪除操做。

func run() error {
        mux := makeMuxRouter()
        httpAddr := os.Getenv("ADDR")
        log.Println("Listening on ", os.Getenv("ADDR"))
        s := &http.Server{
                Addr:           ":" + httpAddr,
                Handler:        mux,
                ReadTimeout:    10 * time.Second,
                WriteTimeout:   10 * time.Second,
                MaxHeaderBytes: 1 << 20,
        }

        if err := s.ListenAndServe(); err != nil {
                return err
        }

        return nil
}

func makeMuxRouter() http.Handler {
        muxRouter := mux.NewRouter()
        muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
        muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
        return muxRouter
}
複製代碼

httpAddr := os.Getenv("ADDR") 將會從剛纔咱們建立的 .env 文件中拉取端口 :8080。咱們就能夠經過訪問瀏覽器的 [http://localhost:8080](http://localhost:8080) 來訪問應用。

讓咱們寫 GET 處理函數來在瀏覽器上打印出區塊鏈。咱們也將會添加一個簡易 respondwithJSON 函數,它會在調用接口發生錯誤的時候,以 JSON 格式反饋給咱們錯誤消息。

func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
        bytes, err := json.MarshalIndent(Blockchain, "", " ")
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        io.WriteString(w, string(bytes))
}

func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
        w.Header().Set("Content-Type", "application/json")
        response, err := json.MarshalIndent(payload, "", " ")
        if err != nil {
                w.WriteHeader(http.StatusInternalServerError)
                w.Write([]byte("HTTP 500: Internal Server Error"))
                return
        }
        w.WriteHeader(code)
        w.Write(response)
}
複製代碼

記住,若是以爲這部分講解太過粗略,請參考在此以前的文章,這裏更詳細的解釋了這部分的每一個步驟。

如今來寫 POST 處理函數。這個函數就是咱們添加新區塊的方法。咱們用 Postman 發送一個 POST 請求,發送一個 JSON 的 body,好比 {「BPM」:60},到 [http://localhost:8080](http://localhost:8080),而且攜帶你以前測得的你的心率。

func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        var m Message

        decoder := json.NewDecoder(r.Body)
        if err := decoder.Decode(&m); err != nil {
                respondWithJSON(w, r, http.StatusBadRequest, r.Body)
                return
        }   
        defer r.Body.Close()

        //ensure atomicity when creating new block
        mutex.Lock()
        newBlock := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
        mutex.Unlock()

        if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
                Blockchain = append(Blockchain, newBlock)
                spew.Dump(Blockchain)
        }   

        respondWithJSON(w, r, http.StatusCreated, newBlock)

}
複製代碼

注意到 mutex 的 lock(加鎖) 和 unlock(解鎖)。在寫入一個新的區塊以前,須要給區塊鏈加鎖,不然多個寫入將會致使數據競爭。精明的讀者還會注意到 generateBlock 函數。這是處理工做量證實的關鍵函數。咱們稍後講解這個。

基本的區塊鏈函數

在開始工做量證實算法以前,咱們先將基本的區塊鏈函數鏈接起來。咱們將會添加一個 isBlockValid 函數,來保證索引正確遞增以及當前區塊的 PrevHash 和前一區塊的 Hash 值是匹配的。

咱們也要添加一個 calculateHash 函數,生成咱們須要用來建立 HashPrevHash 的哈希值。它就是一個索引、時間戳、BPM、前一區塊哈希和 Nonce 的 SHA-256 哈希值(咱們稍後將會解釋它是什麼)。

func isBlockValid(newBlock, oldBlock Block) bool {
        if oldBlock.Index+1 != newBlock.Index {
                return false
        }

        if oldBlock.Hash != newBlock.PrevHash {
                return false
        }

        if calculateHash(newBlock) != newBlock.Hash {
                return false
        }

        return true
}

func calculateHash(block Block) string {
        record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash + block.Nonce
        h := sha256.New()
        h.Write([]byte(record))
        hashed := h.Sum(nil)
        return hex.EncodeToString(hashed)
}
複製代碼

工做量證實

讓咱們來看挖礦算法,或者說工做量證實。咱們但願確保工做量證實算法在容許一個新的區塊 Block 添加到區塊鏈 blockchain 以前就已經完成了。咱們從一個簡單的函數開始,這個函數能夠檢查在工做量證實算法中生成的哈希值是否知足咱們設置的要求。

咱們的要求以下所示:

  • 工做量證實算法生成的哈希值必需要以某個特定個數的零開始
  • 零的個數由常數 difficulty 決定,它在程序的一開始定義(在示例中,它是 1)
  • 咱們能夠經過增長難度值讓工做量證實算法變得困難

完成下面這個函數,isHashValid

func isHashValid(hash string, difficulty int) bool {
        prefix := strings.Repeat("0", difficulty)
        return strings.HasPrefix(hash, prefix)
}
複製代碼

Go 在它的 strings 包裏提供了方便的 RepeatHasPrefix 函數。咱們定義變量 prefix 做爲咱們在 difficulty 定義的零的拷貝。下面咱們對哈希值進行驗證,看是否以這些零開頭,若是是返回 True 不然返回 False

如今咱們建立 generateBlock 函數。

func generateBlock(oldBlock Block, BPM int) Block {
        var newBlock Block

        t := time.Now()

        newBlock.Index = oldBlock.Index + 1
        newBlock.Timestamp = t.String()
        newBlock.BPM = BPM
        newBlock.PrevHash = oldBlock.Hash
        newBlock.Difficulty = difficulty

        for i := 0; ; i++ {
                hex := fmt.Sprintf("%x", i)
                newBlock.Nonce = hex
                if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {
                        fmt.Println(calculateHash(newBlock), " do more work!")
                        time.Sleep(time.Second)
                        continue
                } else {
                        fmt.Println(calculateHash(newBlock), " work done!")
                        newBlock.Hash = calculateHash(newBlock)
                        break
                }

        }
        return newBlock
}
複製代碼

咱們建立了一個 newBlock 並將前一個區塊的哈希值放在 PrevHash 屬性裏,確保區塊鏈的連續性。其餘屬性的值就很明瞭了:

  • Index 增量
  • Timestamp 是表明了當前時間的字符串
  • BPM 是以前你記錄下的心率
  • Difficulty 就直接從程序一開始的常量中獲取。在本篇教程中咱們將不會使用這個屬性,可是若是咱們須要作進一步的驗證而且確認難度值對哈希結果固定不變(也就是哈希結果以 N 個零開始那麼難度值就應該也等於 N,不然區塊鏈就是受到了破壞),它就頗有用了。

for 循環是這個函數中關鍵的部分。咱們來詳細看看這裏作了什麼:

  • 咱們將設置 Nonce 等於 i 的十六進制表示。咱們須要一個爲函數 calculateHash 生成的哈希值添加一個變化的值的方法,這樣若是咱們沒能獲取到咱們指望的零前綴數目,咱們就能用一個新的值從新嘗試。這個咱們加入到拼接的字符串中的變化的值 **calculateHash** 就被稱爲「Nonce」
  • 在循環裏,咱們用 i 和以 0 開始的 Nonce 計算哈希值,並檢查結果是否以常量 difficulty 定義的零數目開頭。若是不是,咱們用一個增量 Nonce 開始下一輪循環作再次嘗試。
  • 咱們添加了一個一秒鐘的延遲來模擬解決工做量證實算法的時間
  • 咱們一直循環計算直到咱們獲得了咱們想要的零前綴,這就意味着咱們成功的完成了工做量證實。當且僅當這以後才容許咱們的 Block 經過 handleWriteBlock 處理函數被添加到 blockchain

咱們已經寫完了全部函數,如今咱們來完成 main 函數:

func main() {
        err := godotenv.Load()
        if err != nil {
                log.Fatal(err)
        }   

        go func() {
                t := time.Now()
                genesisBlock := Block{}
                genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), "", difficulty, ""} 
                spew.Dump(genesisBlock)

                mutex.Lock()
                Blockchain = append(Blockchain, genesisBlock)
                mutex.Unlock()
        }() 
        log.Fatal(run())

}
複製代碼

咱們使用 godotenv.Load() 函數加載環境變量,也就是用來在瀏覽器訪問的 :8080 端口。

一個 go routine 建立了創世區塊,由於咱們須要它做爲區塊鏈的起始點

咱們用剛纔建立的 run() 函數開始網絡服務。

完成了!是時候運行它了!

這裏有完整的代碼。

讓咱們試着運行這個寶寶!

go run main.go 來開始程序

而後用瀏覽器訪問 [http://localhost:8080](http://localhost:8080)

創世區塊已經爲咱們建立好。如今打開 Postman 而後發送一個 POST 請求,向同一個路由以 JSON 格式在 body 中發送以前測定的心率值。

發送請求以後,在終端看看發生了什麼。你將會看到你的機器忙着用增長 Nonce 值不停建立新的哈希值,直到它找到了須要的零前綴值。

當工做量證實算法完成了,咱們就會獲得一條頗有用的 work done! 消息,咱們就能夠去檢驗哈希值來看看它是否是真的以咱們設置的 difficulty 個零開頭。這意味着理論上,那個咱們試圖添加 BPM = 60 信息的新區塊已經被加入到咱們的區塊鏈中了。

咱們來刷新瀏覽器並查看:

成功了!咱們的第二個區塊已經被加入到創世區塊以後。這意味着咱們成功的在 POST 請求中發送了區塊,這個請求觸發了挖礦的過程,而且當且僅當工做量證實算法完成後,它纔會被添加到區塊鏈中。

接下來

很棒!剛纔你學到的真的很重要。工做量證實算法是比特幣,以太坊以及其餘不少大型區塊鏈平臺的基礎。咱們剛纔學到的並不是小事;雖然咱們在示例中使用了一個很低的 difficulty 值,可是將它增長到一個比較大的值就正是生產環境下區塊鏈工做量證實算法是如何運做的。

如今你已經清楚瞭解了區塊鏈技術的核心部分,接下來如何學習將取決於你。我向你推薦以下資源:

  • 在咱們的 Networking tutorial 教程中學習聯網區塊鏈如何工做。
  • 在咱們的 IPFS tutorial 教程中學習如何以分佈式存儲大型文件並用區塊鏈通訊。

若是你作好準備作另外一次技術上的跳躍,試着學習 股權證實(Proof of Stake) 算法。雖然大多數的區塊鏈使用工做量證實算法做爲共識算法,股權證實算法正得到愈來愈多的關注。不少人相信以太坊未來會從工做量證實算法切換到股權證實算法。

想看關於工做量證實算法和股權證實算法的比較教程?在上面的代碼中發現了錯誤?喜歡咱們作的事?討厭咱們作的事?

經過 加入咱們的 Telegram 消息羣 讓咱們知道你的想法!你將獲得本教程做者以及 Coral Health 團隊其餘成員的熱情應答。

想要了解更多關於 Coral Health 以及咱們如何使用區塊鏈來改進我的醫藥研究,訪問咱們的網站


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索