天天看點

【轉】200行代碼實作一個區塊鍊之一-----最簡單的區塊鍊

隻用200行Go代碼寫一個自己的區塊鍊!

2018-01-30 12:49 Coral Health

http://www.10tiao.com/html/175/201801/2653549361/1.html#comment 1

  閱讀 203

區塊鍊是目前最熱門的話題,廣大讀者都聽說過比特币,或許還有智能合約,相信大家都非常想了解這一切是如何工作的。這篇文章就是幫助你使用 Go 語言來實作一個簡單的區塊鍊,用不到 200 行代碼來揭示區塊鍊的原理!高可用架構也會持續推出更多區塊鍊方面文章,歡迎點選上方藍色『高可用架構』關注。

“用不到200行 Go 代碼就能實作一個自己的區塊鍊!” 聽起來有意思嗎?有什麼能比開發一個自己的區塊鍊更好的學習實踐方法呢?那我們就一起來實踐下!

因為我們是一家從事醫療健康領域的科技公司,是以我們采用人類平靜時的心跳資料(BPM心率)作為這篇文章中的示例資料。讓我們先來統計一下你一分鐘内的心跳數,然後記下來,這個數字可能會在接下來的内容中用到。

通過本文,你将可以做到:

  • 建立自己的區塊鍊
  • 了解 hash 函數是如何保持區塊鍊的完整性
  • 如何創造并添加新的塊
  • 多個節點如何競争生成塊
  • 通過浏覽器來檢視整個鍊
  • 所有其他關于區塊鍊的基礎知識

但是,對于比如工作量證明算法(PoW)以及權益證明算法(PoS)這類的共識算法文章中将不會涉及。同時為了讓你更清楚得檢視區塊鍊以及塊的添加,我們将網絡互動的過程簡化了,關于 P2P 網絡比如“全網廣播”這個過程等内容将在下一篇文章中補上。

讓我們開始吧!

設定

我們假設你已經具備一點 Go 語言的開發經驗。在安裝和配置 Go 開發環境後之後,我們還要擷取以下一些依賴:

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

spew 可以幫助我們在 console 中直接檢視 struct 和 slice 這兩種資料結構。

go get github.com/gorilla/mux      

Gorilla 的 mux 包非常流行, 我們用它來寫 web handler。

go get github.com/joho/godotenv      

godotenv 可以幫助我們讀取項目根目錄中的 .env 配置檔案,這樣我們就不用将 http port 之類的配置寫死進代碼中了。比如像這樣:

ADDR=8080      

接下來,我們建立一個 main.go 檔案。之後我們的大部分工作都圍繞這個檔案,讓我開始編碼吧!

導入依賴

我們将所有的依賴包以聲明的方式導入進去:

package main     import (        "crypto/sha256"        "encoding/hex"        "encoding/json"        "io"        "log"        "net/http"        "os"        "time"        "github.com/davecgh/go-spew/spew"        "github.com/gorilla/mux"        "github.com/joho/godotenv"     )      

資料模型

接着我們來定義一個結構體,它代表組成區塊鍊的每一個塊的資料模型:

type Block struct {     Index     int     Timestamp string     BPM       int     Hash      string     PrevHash  string     }      
  • Index 是這個塊在整個鍊中的位置
  • Timestamp 顯而易見就是塊生成時的時間戳
  • Hash 是這個塊通過 SHA256 算法生成的散列值
  • PrevHash 代表前一個塊的 SHA256 散列值
  • BPM 每分鐘心跳數,也就是心率。還記得文章開頭說到的嗎?

接着,我們再定義一個結構表示整個鍊,最簡單的表示形式就是一個 Block 的 slice:

var Blockchain []Block      

我們使用雜湊演算法(SHA256)來确定和維護鍊中塊和塊正确的順序,確定每一個塊的 PrevHash 值等于前一個塊中的 Hash 值,這樣就以正确的塊順序建構對外連結:

散列和生成塊

我們為什麼需要散列?主要是兩個原因:

  • 在節省空間的前提下去唯一辨別資料。散列是用整個塊的資料計算得出,在我們的例子中,将整個塊的資料通過 SHA256 計算成一個定長不可僞造的字元串。
  • 維持鍊的完整性。通過存儲前一個塊的散列值,我們就能夠確定每個塊在鍊中的正确順序。任何對資料的篡改都将改變散列值,同時也就破壞了鍊。以我們從事的醫療健康領域為例,比如有一個惡意的第三方為了調整“人壽險”的價格,而修改了一個或若幹個塊中的代表不健康的 BPM 值,那麼整個鍊都變得不可信了。

我們接着寫一個函數,用來計算給定的資料的 SHA256 散列值:

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

這個 calculateHash 函數接受一個塊,通過塊中的 Index,Timestamp,BPM,以及 PrevHash 值來計算出 SHA256 散列值。接下來我們就能便攜一個生成塊的函數:

func generateBlock(oldBlock Block, BPM int) (Block, error) {     var newBlock Block     t := time.Now()     newBlock.Index = oldBlock.Index + 1     newBlock.Timestamp = t.String()     newBlock.BPM = BPM     newBlock.PrevHash = oldBlock.Hash     newBlock.Hash = calculateHash(newBlock)     return newBlock, nil     }      

其中,Index 是從給定的前一塊的 Index 遞增得出,時間戳是直接通過 time.Now() 函數來獲得的,Hash 值通過前面的 calculateHash 函數計算得出,PrevHash 則是給定的前一個塊的 Hash 值。

校驗塊

搞定了塊的生成,接下來我們需要有函數幫我們判斷一個塊是否有被篡改。檢查 Index 來看這個塊是否正确得遞增,檢查 PrevHash 與前一個塊的 Hash 是否一緻,再來通過 calculateHash 檢查目前塊的 Hash 值是否正确。通過這幾步我們就能寫出一個校驗函數:

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 replaceChain(newBlocks []Block) {     if len(newBlocks) > len(Blockchain) {     Blockchain = newBlocks     }     }      

到這一步,我們基本就把所有重要的函數完成了。接下來,我們需要一個友善直覺的方式來檢視我們的鍊,包括資料及狀态。通過浏覽器檢視 web 頁面可能是最合适的方式!

Web 服務

我猜你一定對傳統的 web 服務及開發非常熟悉,是以這部分你肯定一看就會。

借助 Gorilla/mux 包,我們先寫一個函數來初始化我們的 web 服務:

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     }      

其中的端口号是通過前面提到的 .env 來獲得,再添加一些基本的配置參數,這個 web 服務就已經可以 listen and serve 了!

接下來我們再來定義不同 endpoint 以及對應的 handler。例如,對“/”的 GET 請求我們可以檢視整個鍊,“/”的 POST 請求可以建立塊。

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

GET 請求的 handler:

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

為了簡化,我們直接以 JSON 格式傳回整個鍊,你可以在浏覽器中通路 localhost:8080 或者 127.0.0.1:8080 來檢視(這裡的8080就是你在 .env 中定義的端口号 ADDR)。

POST 請求的 handler 稍微有些複雜,我們先來定義一下 POST 請求的 payload:

type Message struct {     BPM int     }      

再看看 handler 的實作:

func handleWriteBlock(w http.ResponseWriter, r *http.Request) {     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()     newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)     if err != nil {     respondWithJSON(w, r, http.StatusInternalServerError, m)     return     }     if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {     newBlockchain := append(Blockchain, newBlock)     replaceChain(newBlockchain)     spew.Dump(Blockchain)     }     respondWithJSON(w, r, http.StatusCreated, newBlock)     }      

我們的 POST 請求體中可以使用上面定義的 payload,比如:

{"BPM":75}      

還記得前面我們寫的 generateBlock 這個函數嗎?它接受一個“前一個塊”參數,和一個 BPM 值。POST handler 接受請求後就能獲得請求體中的 BPM 值,接着借助生成塊的函數以及校驗塊的函數就能生成一個新的塊了!

除此之外,你也可以:

  • 使用spew.Dump 這個函數可以以非常美觀和友善閱讀的方式将 struct、slice 等資料列印在控制台裡,友善我們調試。
  • 測試 POST 請求時,可以使用 POSTMAN 這個 chrome 插件,相比 curl它更直覺和友善。

POST 請求處理完之後,無論建立塊成功與否,我們需要傳回用戶端一個響應:

func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {     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)     }      

快要大功告成了

接下來,我們把這些關于區塊鍊的函數,web 服務的函數“組裝”起來:

func main() {     err := godotenv.Load()     if err != nil {     log.Fatal(err)     }     go func() {     t := time.Now()     genesisBlock := Block{0, t.String(), 0, "", ""}     spew.Dump(genesisBlock)     Blockchain = append(Blockchain, genesisBlock)     }()     log.Fatal(run())     }      

這裡的 genesisBlock (創世塊)是 main 函數中最重要的部分,通過它來初始化區塊鍊,畢竟第一個塊的 PrevHash 是空的。

哦耶!完成了

你們可以從這裡獲得完整的代碼:Github repo[1]

讓我們來啟動它:

go run main.go      

在終端中,我們可以看到 web 伺服器啟動的日志資訊,并且列印出了創世塊的資訊:

接着我們打開浏覽器,通路 localhost:8080 這個位址,我們可以看到頁面中展示了目前整個區塊鍊的資訊(當然,目前隻有一個創世塊):

接着,我們再通過 POSTMAN 來發送一些 POST 請求:

重新整理剛才的頁面,現在的鍊中多了一些塊,正是我們剛才生成的,同時你們可以看到,塊的順序和散列值都正确。

下一步

剛剛我們完成了一個自己的區塊鍊,雖然很簡單(陋),但它具備塊生成、散列計算、塊校驗等基本能力。接下來你就可以繼續深入的學習區塊鍊的其他重要知識,比如工作量證明、權益證明這樣的共識算法,或者是智能合約、Dapp、側鍊等等。

目前這個實作中不包括任何 P2P 網絡的内容,我們會在下一篇文章中補充這部分内容,當然,我們鼓勵你在這個基礎上自己實踐一遍!

原文釋出時間為:2018年03月24日

本文作者:阿卡司機

本文來源:

CSDN

,如需轉載請聯系原作者。

上一篇: tr cells 集合
下一篇: tr ch 屬性

繼續閱讀