如何使用Go語言編寫自己的區塊鏈挖礦演算法
譯文聲明本文是翻譯文章,文章原作者Coral Health,文章來源:http://medium.com
原文地址:https://medium.com/@mycoralhealth/code-your-own-blockchain-mining-algorithm-in-go-82c6a71aba1f
一、前言
隨著近期比特幣(Bitcoin)以及以太坊(Ethereum)挖礦熱潮的興起,人們很容易對此浮想聯翩。對於剛進入該領域的新手而言,他們會聽到各種故事,比如許多人將GPU堆滿倉庫來瘋狂挖礦,每月能挖到價值數百萬美元的加密貨幣(cryptocurrency)。那麼就是什麼是密幣挖礦?挖礦的原理是什麼?怎麼樣才能編寫自己的挖礦演算法?
在本文中,我們會給大家一一解答這些問題,然後介紹如何編寫自己的挖礦演算法。我們將這種演算法稱為Proof of Work(工作量證明)演算法,該演算法是比特幣及以太坊這兩種最流行的加密貨幣的基礎。無需擔憂,我們會給大家介紹得明明白白。
二、什麼是密幣挖礦
物以稀為貴,對加密貨幣來說也是如此。如果任何人在任何時間都可以隨意生產任意多的比特幣,那麼比特幣作為一種貨幣則毫無價值可言(稍等,這不正是美聯儲常乾的事嗎……)。比特幣演算法每隔10分鐘就會向比特幣網路中的獲勝成員頒發一次比特幣,總量大約在122年內會達到最大值。這種頒發機制可以將通貨膨脹率控制在一定範圍內,因為演算法並沒有在一開始就給出所有密幣,而是隨著時間推移逐步產出。
在這種演算法下,為了獲取比特幣獎勵,礦工們需要付出「勞動」,與其他礦工競爭。這個過程也稱為「挖礦」,因為該過程類似於黃金礦工的採礦過程,為了找到一點黃金,工人們也需要付出時間及精力最終才能獲取勝利果實(有時候也會竹籃打水一場空)。
比特幣演算法會強制參與者(或節點)進行挖礦,同時相互競爭,確保比特幣產出速度不至於太快。
三、如何挖礦
如果在Google上搜索「如何挖比特幣?」,我們會看到大量結果,紛紛解釋挖掘比特幣需要節點(用戶或者主機)來解決複雜的數學難題。雖然這種說法從技術角度來看沒有問題,但簡單稱之為「數學」問題顯然有點太過於僵硬,挖礦的過程其實非常有趣。我們要了解一些密碼學知識以及哈希演算法,才能理解挖礦的原理。
密碼學簡介
單向加密以人眼可讀的數據作為輸入(如「Hello world」),然後通過某個函數生成難以辨認的輸出。這些函數(或者演算法)在性質及複雜度上各不相同。演算法越複雜,想逆向分析就越難。因此,加密演算法在保護數據(如用戶密碼以及軍事代碼)方面至關重要。
以非常流行的SHA-256演算法為例,我們可以使用哈希生成網站來生成SHA-256哈希值。比如,我們可以輸入「Hello world」,觀察對應的哈希結果:
我們可以重複計算「Hello world」的哈希值,每次都能得到相同的結果。在編程中,輸入相同的情況下,如果每次都得到同樣的輸出,這個過程稱之為「冪等性(idempotency)」。
對於加密演算法而言,我們很難通過逆向分析來推導原始輸入,但很容易驗證輸出結果是否正確,這是加密演算法的一個基本特性。比如前面那個例子,我們很容易就能驗證「Hello world」的SHA-256哈希值是否正確,但很難通過給定的哈希值來推導原始輸入為「Hello world」。這也是我們為何將這類加密方法稱為單向加密的原因所在。
比特幣使用的是雙重SHA-256演算法(Double SHA-256),也就是說它會將「Hello world」的SHA-256哈希值作為輸入,再計算一次SHA-256哈希。在本文中,為了方便起見,我們只計算一次SHA-256哈希。
挖礦
理解加密演算法後,我們可以回到密幣挖礦這個主題上。比特幣需要找到一些方法,讓希望獲得比特幣的參與者能通過加密演算法來「挖礦」,以便控制比特幣的產出速度。具體說來,比特幣會讓參與者計算一堆字母和數字的哈希值,直到他們算出的哈希值中「前導0」的位數超過一定長度為止。
比如,回到前面那個哈希計算網站,輸入「886」後我們可以得到前綴為3個0的哈希值。
問題是我們如何才能知道「886」的哈希值開頭包含3個零呢?這才是重點。在撰寫本文之前,我們沒有辦法知道這一點。從理論上講,我們必須嘗試字母和數字的各種組合,對結果進行測試,直到獲得滿足要求的結果為止。這裡我們事先給出了「886」的哈希結果以便大家理解。
任何人都能很容易驗證出「886」是否會生成3位前導零的哈希結果,這樣就能證明我們的確做了大量測試、檢查了大量字母和數字的組合,最終得到了這一結果。因此,如果我們是第一個獲得這個結果的人,其他人可以快速驗證「886」的正確性,這個工作量的證明過程可以讓我贏得比特幣。這也是為什麼人們把比特幣的一致性演算法稱之為Proof-of-Work(工作量證明)演算法。
但如果我的運氣爆表,第一次嘗試就生成了3個前導零結果呢?這種事情基本不可能發生,偶然的節點在首次嘗試時就成功挖掘新塊(向大家證明他們的確做了這項工作)的可能性微乎其微,相反其他數百萬個節點需要付出大量工作才能找到所需的散列。你可以繼續嘗試一下,隨便輸入一些字母和數字的組合,我打賭你得不到3個前導零的結果。
比特幣的約束條件比這個例子更加複雜(要求得到更多位前導零!),並且它能動態調整難度,以確保工作量不會太輕或者太重。請記住,比特幣演算法的目標是每隔10分鐘產出一枚比特幣。因此,如果太多人參與挖礦,比特幣需要加大工作量證明難度,實現難度的動態調整。從我們的角度來看,調整難度等同於增加更多位前導零。
因此,大家可以明白比特幣的一致性演算法比單純「解決數學難題」要有趣得多。
四、開始編程
背景知識已經介紹完畢,現在我們可以使用Proof-of-Work演算法來構建自己的區塊鏈(Blockchain)程序。我選擇使用Go語言來實現,這是一門非常棒的語言。
在繼續之前,我建議你閱讀我們之前的一篇博客:<a href=」https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc」>《用200行Go代碼實現自己的區塊鏈》。這不是硬性要求,但我們會快速掠過以下某些例子,如果你想了解更多細節,可以參考那篇文章。如果你已經了如指掌,可以直接跳到「Proof of Work」章節。
整體架構如下:
我們需要一台Go伺服器,為了簡單起見,我會將所有代碼放在一個main.go
文件中。這個文件提供了關於區塊鏈邏輯的所有信息(包括Proof of Work),也包含所有REST API對應的處理程序。區塊鏈數據是不可改變的,我們只需要GET
以及POST
請求即可。我們需要使用瀏覽器發起GET請求來查看數據,使用Postman來POST
新的區塊(也可以使用curl
)。
導入依賴
首先我們需要導入一些庫,請使用go get
命令獲取如下包:
1、github.com/davecgh/go-spew/spew
:幫助我們在終端中直接查看結構規整的區塊鏈信息。
2、github.com/gorilla/mux
:用來搭建Web伺服器的一個庫,非常好用。
3、github.com/joho/godotenv
:可以從根目錄中的.env
文件中讀取環境變數。
首先我們可以在根目錄中創建一個.env
文件,用來存放後面需要的一個環境變數。.env
文件只有一行:ADDR=8080
。
在根目錄的main.go
中,將依賴包以聲明的方式導入:
package mainimport ( "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")
如果你看過我們前面那篇<a href=」https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc」>文章,那麼對下面這張圖應該不會感到陌生。我們使用散列演算法來驗證區塊鏈中區塊的正確性,具體方法是將某個區塊中的previous hash與前一個區塊的hash進行對比,確保兩者相等。這樣就能維護區塊鏈的完整性,並且惡意成員無法篡改區塊鏈的歷史信息。
其中,BPM
指的是脈搏速率,或者每分鐘的跳動次數。我們將使用你的心率來作為區塊中的一部分數據。你可以將兩根手指放在手腕內側,計算一分鐘內感受到多少次脈動,記住這個數字即可。
基本概念
現在,在main.go
的聲明語句後面添加數據模型以及後面需要用到的其他變數。
const difficulty = 1type Block struct { Index int Timestamp string BPM int Hash string PrevHash string Difficulty int Nonce string}var Blockchain []Blocktype Message struct { BPM int}var mutex = &sync.Mutex{}
difficulty
是一個常量,用來定義哈希值中需要的前導零位數。前導零位數越多,我們越難找到正確的哈希值。我們先從1位前導零開始。
Block
是每個區塊的數據模型。不要擔心Nonce
欄位,後面我們會介紹。
Blockchain
由多個Block
組成,代表完整的區塊鏈。
我們會在REST API中,使用POST
請求提交Message
以生成新的Block
。
我們聲明了一個互斥量mutex
,後面我們會使用該變數避免出現數據競爭,確保多個區塊不會同一時間生成。
Web伺服器
讓我們快速搭一個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/
這個地址來訪問我們構建的應用。
現在我們需要編寫GET
處理函數,在瀏覽器中顯示區塊鏈信息。我們還需要添加一個respondwithJSON
函數,一旦API調用過程中出現錯誤就能以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)}
請注意:如果覺得我們講得太快,可以參考之前的<a href=」https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc」>文章,文章中詳細解釋了每個步驟。
現在編寫POST
處理函數,這個函數可以實現新區塊的添加過程。我們使用Postman來發起POST
請求,向http://localhost:8080
發送JSON數據(如{「BPM」:60}
),其中包含前面你記錄下的那個脈搏次數。
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
函數,這是處理Proof of Work的關鍵函數,稍後我們會介紹。
基本的區塊鏈函數
在介紹Proof of Work之前,先整理下基本的區塊鏈函數。我們添加了一個isBlockValid
函數,確保我們的索引能正確遞增,並且當前區塊的PrevHash
與前一個區塊的Hash
相匹配。
我們也添加了一個calculateHash
函數,用來生成哈希值,以計算Hash
以及PrevHash
。我們將Index
、Timestamp
、BPM
、PrevHash
以及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)}
五、Proof of Work
現在來介紹挖礦演算法,也就是Proof of Work。在新的Block
加入blockchain
之前,我們需要確保Proof of Work任務已經完成。我們先以一個簡單的函數開始,該函數可以檢查Proof of Work過程中生成的哈希是否滿足我們設置的約束條件。
我們的約束條件如下:
1、Proof of Work生成的哈希必須具有特定位數的前導零。
2、前導零的位數由程序剛開始定義的difficulty
常量來決定(這裡這個值為1).
3、我們可以增加難度值來提高Proof of Work的難度。
首先構造一個函數:isHashValid
:
func isHashValid(hash string, difficulty int) bool { prefix := strings.Repeat("0", difficulty) return strings.HasPrefix(hash, prefix)}
Go語言在strings
包中提供了Repeat
以及HasPrefix
函數,使用起來非常方便。我們定義了一個prefix
變數,用來表示前導零位數,然後檢查哈希值的前導零位數是否滿足要求,滿足則返回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
欄位中,確保區塊鏈滿足連續性要求。其他欄位的含義應該比較明顯:
1、Index
不斷增加;
2、Timestamp
是當前時間的字元串表現形式;
3、BPM
是前面記錄下的心率;
4、Difficulty
直接摘抄自最開頭定義的那個常量。這個例子中我們不會使用這個欄位,但如果我們想進一步驗證,確保難度值與哈希結果一致(也就是說哈希結果的前導零位數為N,那麼Difficulty
的值應該也為N,否則區塊鏈就會遭到破壞),那麼這個欄位就非常有用。
這個函數中的for
循環非常重要,我們詳細介紹一下:
1、獲取i
的十六進位形式,將該值賦給Nonce
。我們需要一種方法來將動態變化的一個值加入哈希結果中,這樣如果我們沒有得到理想的哈希值,就可以通過calculateHash
函數繼續生成新的值。我們在calculateHash
計算過程中添加的動態值就稱為「Nonce」。
2、在循環中,我們的i
和Nonce值從0開始遞增,判斷生成的哈希結果中前導零位數是否與difficulty
相等。如果不相等,我們進入新的迭代,增加Nonce,再次計算。
3、我們加了1秒的休眠操作,模擬解決Proof of Work所需的時間。
4、繼續循環,直到計算結果中包含特定位數的前導零為止,此時我們成功完成了Proof of Work任務。只有在這種情況下,我們的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常式來創建創世區塊。
然後使用前面構造的run()
函數啟動Web伺服器。
六、大功告成
大家可以訪問Github獲取完整版代碼。
來跑一下看看效果。
首先使用go run main.go
命令啟動程序。
然後在瀏覽器中訪問http://localhost:8080
:
區塊鏈中已經有一個創世區塊。現在打開Postman,通過POST請求向伺服器發送包含BPM欄位(心率數據)的JSON數據。
發送完請求後,我們可以在終端中觀察操作結果。可以看到,我們的計算機會使用不斷遞增的Nonce值來創建新的哈希,直到計算出來的哈希滿足前導零位數要求為止。
完成Proof of Work任務後,我們可以看到一則提示消息:work done!
。我們可以檢驗這個哈希值,發現它的確滿足difficulty
所設置的前導零位數要求。這意味著從理論上來講,我們使用BPM = 60
參數所生成的新區塊現在應該已經添加到區塊鏈中。
刷新瀏覽器看一下結果:
大功告成!我們的第二個區塊已經添加到創世區塊後面。也就是說,我們成功通過POST
請求發送了區塊,這個操作觸發了挖礦過程,當且僅當Proof of Work得以解決時,新的區塊才能添加到區塊鏈中。
七、下一步考慮
非常好,前面學到的知識非常重要。Proof of Work是比特幣、以太坊以及其他區塊鏈平台的基礎。我們前面的分析意義非凡,雖然這個例子使用的難度值並不大,但實際環境中Proof of Work區塊鏈的工作原理就是把難度提高到較高水平而已。
現在你已經深入了解了區塊鏈技術的關鍵組成部分,後面的路需要你自己去走,我們建議你可以繼續學習以下知識:
1、學習區塊鏈網路的工作原理,可以參考我們的<a href=」https://medium.com/@mycoralhealth/part-2-networking-code-your-own-blockchain-in-less-than-200-lines-of-go-17fe1dad46e1″>網路教程。
2、學習如何以分散式方式存儲大型文件並與區塊鏈交互,可以參考我們的<a href=」https://medium.com/@mycoralhealth/learn-to-securely-share-files-on-the-blockchain-with-ipfs-219ee47df54c」>IPFS教程。
如果你還想繼續深入學習,可以考慮了解一下Proof of Stake(權益證明)。儘管目前大多數區塊鏈使用Proof of Work作為一致性演算法,但公眾越來越關注Proof of Stake。人們普遍認為,未來以太坊將從Proof of Work遷移至Proof of Stake。
本文翻譯自:medium.com
如若轉載,請註明出處:medium.com安全客 - 有思想的安全新媒體
推薦閱讀:
※如何用雲挖礦平台hashflare挖礦賺比特幣(附教程)
※CPU挖礦 讓自家的本本跑起來,BTH
※NrsMiner:一個構造精密的挖礦殭屍網路
※我的世界手機版用什麼挖礦?