1. 程式人生 > >200行go程式碼實現區塊鏈

200行go程式碼實現區塊鏈

你可以學到什麼

  • 建立自己的blockchain
  • 理解雜湊演算法是怎樣保證blockchain的完整性
  • 理解新塊是如何被新增的
  • 如何解決多個節點競爭問題
  • 通過瀏覽器來檢視你的blockchain
  • 寫新的blocks
  • 對blockchain有基本的瞭解

本文不包括以下內容

為了簡單起見,本文內容不包括一致性演算法如POW、POS,網路廣播等內容。

開始

配置

因為我們要用GO語言實現,所以這裡假設各位已經有了一定go語言程式設計的基礎,首先我們需要下載幾個模組

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

spew可以理解為一種結構化輸出工具,可以讓我們更清晰地檢視structs 和slices資料。

go get github.com/gorilla/mux

Gorilla/mux 是一個用於Web開發的元件

go get github.com/joho/godoenv

Gotdotenv 是一個讀取在專案根目錄的.env檔案中的配置資訊的元件,本文中我們的web服務埠就定義在該檔案中

現在我們建立一個.env檔案並寫入

ADDR=8080

建立一個main.go檔案,我們所有的程式碼將會寫入該檔案中,並且不會多於200行。

匯入模組

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
}

Block用於儲存要寫入的區塊鏈中的資料,每一個欄位的意思如下:
- Index 表示區塊號(區塊高度)
- Timestamp 自動產生的表示該塊的生成時間
- BPM 可以理解為寫入區塊的資料
- Hash 本塊的雜湊值
- PrevHash 前一個塊的雜湊值

然後建立一個Block的陣列切片用於代表區塊鏈

var Blockchain []Block

雜湊化和產生新塊

為什麼要對資料進行雜湊化,有兩個主要原因
- 節省空間,雜湊值由區塊中所有資料計算而來,在我們的例子中,我們每個塊中都只有很少量的資料,但假如我們的區塊中有成百上千條資料,我們用其雜湊值做標識明顯更有效率,你總不能在隨後的塊中儲存前面塊的所有資料吧?
- 保護區塊完整性,像我們這樣儲存前一個區塊的雜湊值,我們就很容易的可以檢查到前一個區塊有沒有被篡改(因為篡改後雜湊值變化,要麼是前一個塊自檢無法通過,後邊的塊永遠會指向正確的前一個塊,而不會指向被惡意篡改後的。)

我們現在實現一個計算雜湊值的方法

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

我們使用到了Index、Timestamp、BPM、PrevHash 欄位用於計算當前塊的雜湊值

我們再寫一個用於產生新塊的方法

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
}

程式碼很簡單,就不廢話了。

區塊正確性驗證


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 服務

這裡假設您已經熟悉,並有一定的GO 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
}

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

處理GET請求

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

我們將整個Blokcchain轉換為JSON串作為GET請求的返回值 。

我們使用POST請求新增新塊,求稍微複雜一些,我們需要一個結構體

type Message struct {
    BPM int
}

用作引數傳遞。
處理方法:

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)

}

我們上邊使用了一個封裝後的方法respondWithJson 當出現錯誤的時候返回HTTP:500,成功的話正常返回。

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

最後我們還需要一個Main方法,作為程式入口。

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

}

完成並測試

啟動web服務

 go run main.go

使用瀏覽器訪問我們的web服務,我們可以看到如下結果
這裡寫圖片描述
我們使用POSTMAN測試新增新區塊
這裡寫圖片描述
我們重新整理一下瀏覽器,我們就可以看到我們剛新增的塊
這裡寫圖片描述

最後

這最後一段看得懂,但翻譯好麻煩,我決定給上英文原文了。
Congrats!! You just wrote up your own blockchain with proper hashing and block validation. You should now be able to control your own blockchain journey and explore more complicated topics like Proof of Work, Proof of Stake, Smart Contracts, Dapps, Side Chains and more.

What this tutorial doesn’t address is how new blocks get mined using Proof of Work. This would be a separate tutorial but plenty of blockchains exist without Proof of Work mechanisms. In addition, the network broadcasting is currently simulated by writing and viewing the blockchain in a web server. There is no P2P component in this tutorial.

下面連結是我轉成PDF的英文原文有興趣大家可以閱讀一下。