本文你將用Go語言建立本身的區塊鏈、理解哈希函數是如何保持區塊鏈的完整性、掌握如何創造並添加新的塊、實現多個節點經過競爭生成塊、經過瀏覽器來查看整個鏈、瞭解全部其餘關於區塊鏈的基礎知識。git
可是,文章中將不會涉及工做量證實算法(PoW)以及權益證實算法(PoS)這類的共識算法,同時爲了讓你更清楚得查看區塊鏈以及塊的添加,咱們將網絡交互的過程簡化了,關於 P2P 網絡好比「全網廣播」這個過程等內容將在後續文章中補上。github
咱們假設你已經具有一點 Go 語言的開發經驗。在安裝和配置 Go 開發環境後以後,咱們還要獲取如下一些依賴:web
~$ go get github.com/davecgh/go-spew/spew
spew
能夠幫助咱們在終端中中直接查看 struct 和 slice 這兩種數據結構。算法
~$ go get github.com/gorilla/mux
Gorilla 的 mux
包很是流行, 咱們用它來寫 web handler。chrome
~$ go get github.com/joho/godotenv
godotenv
能夠幫助咱們讀取項目根目錄中的.env
配置文件,這樣就不用將 http端口之類的配置硬編碼進代碼中了。好比像這樣:json
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 }
接着,咱們再定義一個結構表示整個鏈,最簡單的表示形式就是一個 Block 的 slice:數據結構
var Blockchain []Block
咱們使用散列算法(SHA256)來肯定和維護鏈中塊和塊正確的順序,確保每個塊的 PrevHash 值等於前一個塊中的 Hash 值,這樣就以正確的塊順序構建出鏈:
咱們爲何須要散列?主要是兩個緣由:
咱們接着寫一個函數,用來計算給定的數據的 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 服務及開發很是熟悉,因此這部分你確定一看就會。
藉助 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 值,接着藉助生成塊的函數以及校驗塊的函數就能生成一個新的塊了!
除此以外,你也能夠:
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
讓咱們來啓動它:
~$ go run main.go
在終端中,咱們能夠看到 web 服務器啓動的日誌信息,而且打印出了創世塊的信息:
接着咱們打開瀏覽器,訪問 localhost:8080 這個地址,咱們能夠看到頁面中展現了當前整個區塊鏈的信息(固然,目前只有一個創世塊):
接着,咱們再經過 POSTMAN 來發送一些 POST 請求:
刷新剛纔的頁面,如今的鏈中多了一些塊,正是咱們剛纔生成的,同時大家能夠看到,塊的順序和散列值都正確。
剛剛咱們完成了一個本身的區塊鏈,雖然很簡單(陋),但它具有塊生成、散列計算、塊校驗等基本能力。接下來你就能夠繼續深刻的學習 區塊鏈的其餘重要知識,好比工做量證實、權益證實這樣的共識算法,或者是智能合約、Dapp、側鏈等等。
目前這個實現中不包括任何 P2P 網絡的內容,咱們會在下一篇文章中補充這部份內容,固然,咱們鼓勵你在這個基礎上本身實踐一遍!
若是你但願高效的學習以太坊DApp開發,能夠訪問匯智網提供的最熱門在線互動教程:
其餘更多內容也能夠訪問這個以太坊博客。