- 原文地址:Code your own blockchain mining algorithm in Go!
- 原文作者:Coral Health
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:stormluke,mingxing47
如果你對下面的教程有任何問題或者建議,加入我們的 Telegram 訊息群,可以問我們所有你想問的!
隨著最近比特幣和以太坊挖礦大火,很容易讓人好奇,這麼大驚小怪是為什麼。對於加入這個領域的新人,他們會聽到一些瘋狂的故事:人們用 GPU 填滿倉庫,每個月賺取價值數百萬美元的加密貨幣。電子貨幣挖礦到底是什麼?它是如何運作的?我如何能試著編寫自己的挖礦演算法?
在這篇部落格中,我們將會帶你解答上述每一個問題,並最終完成一篇教你如何編寫自己的挖礦演算法的教程。我們將展示給你的演算法叫做工作量證明,它是比特幣和以太坊這兩個最流行的電子貨幣的基礎。別急,我們馬上將為你解釋它是如何運作的。
什麼是電子貨幣挖礦
為了有價值,電子貨幣需要有一定的稀缺性。如果誰都可以隨時生產出他們想要的任意多的比特幣,那麼作為貨幣,比特幣就毫無價值了。(等一下,美國聯邦儲備不是這麼做了麼?打臉)比特幣演算法每十分鐘將會發放一些比特幣給網路中一個獲勝成員,這樣最多可以供給大約 122 年。由於定量的供應並不是在最一開始就全部發行,這種發行時間表在一定程度上也控制了膨脹。隨著時間流逝,發行地速度將越來越慢。
決定勝者是誰並給出比特幣的過程需要他完成一定的“工作”,並與同時也在做這個工作的人競爭。這個過程就叫做挖礦,因為它很像採礦工人花費時間完成工作並最終(希望)找到黃金。
比特幣演算法要求參與者,或者說節點,完成工作並相互競爭,來保證比特幣不會發行過快。
挖礦是如何運作的?
一次谷歌快速搜尋“比特幣挖礦如何運作?”將會給你很多頁的答案,解釋說比特幣挖礦要求節點(你或者說你的電腦)解決一個很難的數學問題。雖然從技術上來說這是對的,但是簡單的把它稱為一個“數學”問題太過有失實質並且太過陳腐。瞭解挖礦運作的內在原理是非常有趣的。為了學習挖礦運作原理,我們首先要了解一下加密演算法和雜湊。
雜湊加密的簡短介紹
單向加密的輸入值是能讀懂的明文,像是“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 介面的處理函式。區塊鏈資料是不可改的,我們只需要 GET
和 POST
請求。我們將用瀏覽器傳送 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
函式,生成我們需要用來建立 Hash
和 PrevHash
的雜湊值。它就是一個索引、時間戳、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
包裡提供了方便的 Repeat
和 HasPrefix
函式。我們定義變數 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 以及我們如何使用區塊鏈來改進個人醫藥研究,訪問我們的網站。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。