只用200行Go代碼寫一個自己的區塊鏈!


 

Coral Health · 大約23小時之前 · 220 次點擊 · 預計閱讀時間 7 分鍾 · 不到1分鍾之前 開始瀏覽    

區塊鏈是目前最熱門的話題,廣大讀者都聽說過比特幣,或許還有智能合約,相信大家都非常想了解這一切是如何工作的。這篇文章就是幫助你使用 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 網絡的內容,我們會在下一篇文章中補充這部分內容,當然,我們鼓勵你在這個基礎上自己實踐一遍!

 

 

相關鏈接

  • https://github.com/mycoralhealth/blockchain-tutorial/blob/master/main.go

 

特別推薦:

 

比特幣、以太坊、ERC20、PoW、PoS、智能合約、閃電網絡……

 

想深入了解及討論這些話題?高可用架構在知識星球(小密圈)創建了區塊鏈學習小組,共同學習區塊鏈包括數字貨幣前沿技術,歡迎點擊鏈接加入。

 

區塊鏈學習小組

 

本文作者 Coral Health,由魏佳翻譯。轉載譯文請注明出處,技術原創及架構實踐文章,歡迎通過公眾號菜單「聯系我們」進行投稿。

 

高可用架構

改變互聯網的構建方式

長按二維碼 關注「高可用架構」公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM