1. 程式人生 > >Go語言實現PoW共識演算法(詳解)

Go語言實現PoW共識演算法(詳解)

PoW呢...Proof of Work ,工作量證明機制,可能這個名字大家不熟悉,說比特幣的話,大家就熟悉了吧,沒錯,PoW就是比特幣所使用的共識機制。

通過計算一個數值( nonce ),使得拼揍上交易資料後內容的 Hash 值滿足規定的上限。在節點成功找到滿足的Hash值之後,會馬上對全網進行廣播打包區塊,網路的節點收到廣播打包區塊,會立刻對其進行驗證。

如果驗證通過,則表明已經有節點成功解迷,自己就不再競爭當前區塊打包,而是選擇接受這個區塊,記錄到自己的賬本中,然後進行下一個區塊的競爭猜謎。 網路中只有最快解謎的區塊,才會新增的賬本中,其他的節點進行復制,這樣就保證了整個賬本的唯一性。

假如節點有任何的作弊行為,都會導致網路的節點驗證不通過,直接丟棄其打包的區塊,這個區塊就無法記錄到總賬本中,作弊的節點耗費的成本就白費了,因此在巨大的挖礦成本下,也使得礦工自覺自願的遵守比特幣系統的共識協議,也就確保了整個系統的安全。

舉個例子啊。栗子

舉個例子,給定的一個基本的字串”Hello, world!”,我們給出的工作量要求是,可以在這個字串後面新增一個叫做nonce的整數值,對變更後(新增nonce)的字串進行SHA256雜湊運算,如果得到的雜湊結果(以16進位制的形式表示)是以”0000”開頭的,則驗證通過。為了達到這個工作量證明的目標。我們需要不停的遞增nonce值,對得到的新字串進行SHA256雜湊運算。按照這個規則,我們需要經過4251次計算才能找到恰好前4位為0的雜湊雜湊。

如圖所示,大概是浙個樣子滴

然後我們今天呢,就用Go語言來實現它。

話不多說,首先從環境搭建教起,對,沒錯,就這個網址

選擇你需要的,筆者話,選擇的是go1.9.2windows-amd64.msi,下載後傻瓜式操作,環境不用配置直接就能用的,這裡展示一下

這樣的話就妥了,然後呢,下載git,這次是這個網址了

下載後開啟,安裝我們本次所需要的三個第三方庫,spew,gorilla/mux,godotenv,這三倒黴孩子有什麼用呢

spew 在控制檯中格式化輸出相應的結果。

gorilla/mux 是編寫web處理程式的流行軟體包。

godotenv 可以從我們專案的根目錄的 .env

 檔案中讀取資料。

事實上如果你打算做公鏈開發相關,這三個依賴是一直都要用的

在git裡分別輸入

$ go get github.com/davecgh/go-spew/spew

$ go get github.com/gorilla/mux

$ go get github.com/joho/godotenv

友情提示這裡會遇到一個很尷尬的問題,bash: $'\302\226go‘: command not found,這個錯誤你猜為啥

只是因為你多加了一個空格在前面而已ummm

好了我們繼續,安裝後環境之後呢

新建一個檔案,叫.env,裡面填ADDR=8080,這是呼叫8080埠的意思,win環境下的話,會提示你必須鍵入檔名,所以我們將檔名修改成.env.   就可以建立.env這種.開頭的檔案了。

再建一個檔案叫,mian.go。這就是咱們的“原始碼”了,開啟它開始程式設計

首先是引入相應的包 ,來咱們逐個解釋一下

package main          //定義報名,package main表示一個可獨立執行的程式,每個 Go 應用程式都包含一個名為 main 的包

import                      //匯入包(的函式或者其他元素)

(

"crypto/sha256"                //軟體包sha256 實現 FIPS 180-4 中定義的 SHA224 和 SHA256 雜湊演算法。

"encoding/hex"                 //包十六進位制實現十六進位制編碼和解碼。

"encoding/json"                /* 包json實現了RFC 4627中定義的JSON的編碼和解碼。JSON和Go值之間的對映在Marshal和                                                Unmarshal函式的文件中進行了描述。

                                          有關此包的介紹,請參閱“JSON和Go”:https://golang.org/doc/articles/json_and_go.html  */

"fmt"                        // fmt 包使用函式實現 I/O 格式化(類似於 C 的 printf 和 scanf 的函式), 格式化引數源自C,但更簡單

"io"                          /* Package io 為 I/O 原語提供基本介面。它的主要工作是將這些原語的現有實現(例如包 os 中的那些                                     原語)包裝到抽象功能的共享公共介面中,以及一些其他相關原語中 */

"log"                        /* Log 包實現了一個簡單的日誌包。它定義了一個型別,記錄器,用於格式化輸出的方法。它還有一個                                 預定義的“standard”記錄器,可以通過幫助函式 Printf|ln,Fatalf|ln 和 Panicf|ln 訪問,比手動建立記錄                                   器更易於使用。該記錄器寫入標準錯誤並列印每條記錄的訊息的日期和時間。每條日誌訊息都在一個單                                 獨的行上輸出:如果正在列印的訊息不以換行符結尾,則記錄器將新增一條。寫入日誌訊息後,致命函                                 數呼叫 os.Exit(1) 。寫入日誌訊息後, Panic 函式呼叫 panic */

"net/http"                 //http包提供HTTP客戶端和伺服器實現

"os"                         /* Package os為作業系統功能提供了一個平臺無關的介面。雖然錯誤處理類似於 Go,但設計類似                                                   Unix,失敗的呼叫返回型別錯誤的值而不是錯誤號  */

"strconv"                 //包strconv實現了對基本資料型別的字串表示的轉換

"strings"                  //打包字串實現簡單的函式來操縱 UTF-8 編碼的字串

"sync"                     /* 程式包 sync 提供基本的同步原語,如互斥鎖。除了 Once 和 WaitGroup 型別之外,大多數型別都是                                    供低階庫例程使用的。通過 Channel 和溝通可以更好地完成更高級別的同步 */

"time"                      // 打包時間提供了測量和顯示時間的功能。日曆計算總是假定公曆,沒有閏秒

"github.com/davecgh/go-spew/spew"     //這三就是剛才那三個倒黴孩子

"github.com/gorilla/mux"

"github.com/joho/godotenv"

)

定義一下區塊中有的

const difficulty = 1    //difficulty 代表難度係數,如果賦值為 1,則需要判斷生成區塊時所產生的 Hash 字首至少包含1個 0

type Block struct      //Block 是我們定義的結構體,它代表組成區塊鏈的每一個塊的資料模型 {

              Index int                //區塊鏈中資料記錄的位置

              Timestamp string    //時間戳,是自動確定的,並且是寫入資料的時間

              Bike int                   //假定我們現在做的是一個共享單車的區塊鏈,Bike就是一定區域內的自行車數量

              Hash string             //是代表這個資料記錄的SHA256識別符號

              PrevHash string      //是鏈中上一條記錄的SHA256識別符號

              Difficulty int             //挖礦的難度

              Nonce string           //PoW中符合條件的數字

}

var Blockchain []Block   // 存放區塊資料

type Message struct{     //  定義結構體,請求的資料

Bike int

}

var mutex = &sync.Mutex{}      //用sync防止同一時間產生多個區塊

定義完成之後,就是該生成區塊了

func generateBlock(oldBlock Block, Bike int) Block {                               //定義函式generateBlock

              var newBlock Block                                                                     //新區塊

              t := time.Now()

              newBlock.Index = oldBlock.Index + 1                                         //區塊的增加,index也加一

              newBlock.Timestamp = t.String()                                                //時間戳

              newBlock.Bike = Bike 

              newBlock.PrevHash = oldBlock.Hash                                         //新區塊的PrevHash儲存上一個區塊的Hash

              newBlock.Difficulty = difficulty                  

              for i := 0; ; i++ {                                                                            //通過迴圈改變 Nonce

                                  hex := fmt.Sprintf("%x", i)

                                  newBlock.Nonce = hex                                             //選出符合難度係數的Nonce

                                  if !isHashValid(calculateHash(newBlock), newBlock.Difficulty) {

                                  //判斷Hash的0的個數,是否與難度係數一致  

                                          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

}

 接著定義之前提到的isHashValid函式,這個函式的作用是判斷Hash的0的個數,是否與難度係數一致 

func isHashValid(hash string, difficulty int) bool {                

                   prefix := strings.Repeat("0", difficulty)                               

                   //複製 difficulty 個0,並返回新字串,當 difficulty 為 4 ,則 prefix 為 0000

                   return strings.HasPrefix(hash, prefix)                                       // 判斷字串 hash 是否包含字首 prefix

}

 接著,我們要開始生成Hash值

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)

}

Hash值完成之後,我們就要來驗證區塊了,定義一個isBlockValid函式

func isBlockValid(newBlock, oldBlock Block) bool {

                   if oldBlock.Index+1 != newBlock.Index { 

                             return false                                  //確認Index的增長正確 

                   }                                                             //雙重否定(笑)

                   if oldBlock.Hash != newBlock.PrevHash {

                             return false                                    //確認PrevHash與前一個塊的Hash相同   

                   }

                   if calculateHash(newBlock) != newBlock.Hash {

                   //在當前塊上 calculateHash 再次執行該函式來檢查當前塊的Hash

                             return false

                   }

                   return true

 ok,區塊的定義到此就告一段落,接下來我們需要定義web伺服器方面

func run() error {                                                 //run函式作為啟動http伺服器的函式

                   mux := makeMuxRouter()               //makeMuxRouter 主要定義路由處理

                   httpAddr := os.Getenv("ADDR")     //.env            

                   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")

                   //當收到GET請求,呼叫handleGetBlockchain函式

                   muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")

                   //當收到POST請求,呼叫handleWriteBlock函式

                   return muxRouter

}

接下來,就是需要在伺服器這邊,遍歷,就是獲取所有區塊的列表資訊,主要作用當然還是處理HTTP的GET請求

func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {         //處理HTTP的GET請求

                   bytes, err := json.MarshalIndent(Blockchain, "", " ")

                   if err != nil {

                             http.Error(w, err.Error(), http.StatusInternalServerError) return

                   }

                   io.WriteString(w, string(bytes))

 handleWriteBlock 主要是生成新的區塊,以及處理HTTP的GET請求

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()  

                   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)

}

 錯誤部分

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                    //如果出錯,返回伺服器500錯誤                    }

                   w.WriteHeader(code)

                   w.Write(response)

}

 然後是主函式。。。說出來你們可能不信,我已經是第四次做到這裡了,csdn的自動儲存真吉爾坑。。。

func main() {

                    err := godotenv.Load()                                   //允許我們讀取.env

                    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())                                                            //啟動web服務

}

okok。。。。第五次了,csdn的自動儲存真。。。牛批,到這裡main.go就寫完了,我們開始測試 首先開啟cmd...命令列工具,進入mian.go所在的目錄

然後執行程式碼,go run main.go

我們可以看到區塊資訊,當然僅有一個區塊肯定是不夠的,運算和驗證都無法完成,這時我們開啟Postman,一個很好用的測試API介面的工具,我們在Postman裡使用POST方式訪問localhost:8080,因為.env檔案呼叫的是8080埠有,隨後在body裡的raw,因為我們是共享單車類的,所以修改Bike的值為100

儲存,讓我們看看命令列這邊會不會開始運算

可以看到一次就完成了word done的挖礦成功,啊我們的運氣真是非常之好,如果在挖比特幣的時候也這麼好運就好了嗯。。。好的因為這一次的巧合,不能算在成功的測試,我們再來一次,將Bike的值修改為120

然後返回命令列

可以看到這次呢,就經過了非常多的運算才完成挖礦,三個區塊都有,讓我們驗證一下

可以看到index的值分別是0,1,2.自增了1,而且第二塊的PrevHash值正是第一塊的Hash值,第三塊同理。

到這裡我們就算是完成了PoW機制的實現,這裡放一下統一的原始碼吧,是沒有註釋的版本

在以後的日子裡,會逐漸更新go語言實現其他的共識機制,到最後會講完全由自己實現的一條公鏈,敬請期待關注

關注微信公眾號IDC大學,最新技術知識及時推送