200行Go代碼實現自己的區塊鏈——區塊生成與網絡通信
在第一篇文章[1]中,我們向大家展示了如何通過精煉的Go代碼實現一個簡單的區塊鏈。如何計算每個塊的 Hash 值,如何驗證塊數據,如何讓塊鏈接起來等等,但是所有這些都是跑在一個節點上的。文章發布後,讀者反響熱烈,紛紛留言讓我快點填坑(網絡部分),於是就誕生了這第二篇文章。
這篇文章在之前的基礎上,解決多個節點網絡內,如何生成塊、如何通信、如何廣播消息等。
流程
- 第一個節點創建“創始區塊”,同時啟動 TCP server並監聽一個端口,等待其他節點連接。
Step 1
- 啟動其他節點,並與第一個節點建立TCP連接(這裏我們通過不同的終端來模擬其他節點)
- 創建新的塊
Step 2
- 第一個節點驗證新生成塊
- 驗證之後廣播(鏈的新狀態)給其他節點
Step 3
- 所有的節點都同步了最新的鏈的狀態
之後你可以重復上面的步驟,使得每個節點都創建TCP server並監聽(不同的)端口以便其他節點來連接。通過這樣的流程你將建立一個簡化的模擬的(本地的)P2P網絡,當然你也可以將節點的代碼編譯後,將二進制程序部署到雲端。
開始coding吧
設置與導入依賴
參考之前第一篇文章,我們使用相同的計算 hash 的函數、驗證塊數據的函數等。
設置
在工程的根目錄創建一個 .env
文件,並添加配置:
ADDR=9000
通過 go-spew
包將鏈數據輸出到控制臺,方便我們閱讀:
go get github.com/davecgh/go-spew/spew
通過 godotenv
包來加載配置文件:
go get github.com/joho/godotenv
之後創建 main.go
文件。
導入
接著我們導入所有的依賴:
package main import ( "bufio" "crypto/sha256" "encoding/hex" "encoding/json" "io" "log" "net" "os" "strconv" "time" "github.com/davecgh/go-spew/spew" "github.com/joho/godotenv" )
回顧
讓我們再快速回顧下之前的重點,我們創建一個 Block
結構體,並聲明一個Block
類型的 slice,Blockchain
:
// Block represents each ‘item‘ in the blockchain type Block struct { Index int Timestamp string BPM int Hash string PrevHash string } // Blockchain is a series of validated Blocks var Blockchain []Block
創建塊時計算hash值的函數:
// SHA256 hashing 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) }
創建塊的函數:
// create a new block using previous block‘s hash 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 }
驗證塊數據的函數:
// make sure block is valid by checking index, // and comparing the hash of the previous block 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 }
確保各個節點都以最長的鏈為準:
// make sure the chain we‘re checking is longer than // the current blockchain func replaceChain(newBlocks []Block) { if len(newBlocks) > len(Blockchain) { Blockchain = newBlocks } }
網絡通信
接著我們來建立各個節點間的網絡,用來傳遞塊、同步鏈狀態等。
我們先來聲明一個全局變量 bcServer
,以 channel(譯者註:channel 類似其他語言中的 Queue,代碼中聲明的是一個 Block 數組的 channel)的形式來接受塊。
// bcServer handles incoming concurrent Blocks var bcServer chan []Block
註:Channel 是 Go 語言中很重要的特性之一,它使得我們以流的方式讀寫數據,特別是用於並發編程。通過這裏[2]可以更深入地學習 Channel。
接下來我們聲明 main
函數,從 .env
加載配置,也就是端口號,然後實例化 bcServer
func main() { err := godotenv.Load() if err != nil { log.Fatal(err) } bcServer = make(chan []Block) // create genesis block t := time.Now() genesisBlock := Block{0, t.String(), 0, "", ""} spew.Dump(genesisBlock) Blockchain = append(Blockchain, genesisBlock) }
接著創建 TCP server 並監聽端口:
// start TCP and serve TCP server server, err := net.Listen("tcp", ":"+os.Getenv("ADDR")) if err != nil { log.Fatal(err) } defer server.Close()
需要註意這裏的 defer server.Close()
,它用來之後關閉鏈接,可以從這裏[3]了解更多 defer
的用法。
for { conn, err := server.Accept() if err != nil { log.Fatal(err) } go handleConn(conn) }
通過這個無限循環,我們可以接受其他節點的 TCP 鏈接,同時通過 go handleConn(conn)
啟動一個新的 go routine(譯者註:Rob Pike 不認為go routine 是協程,因此沒有譯為協程)來處理請求。
接下來是“處理請求”這個重要函數,其他節點可以創建新的塊並通過 TCP 連接發送出來。在這裏我們依然像第一篇文章一樣,以 BPM 來作為示例數據。
- 客戶端通過
stdin
輸入 BPM - 以 BPM 的值來創建塊,這裏會用到前面的函數:
generateBlock
,isBlockValid
,和replaceChain
- 將新的鏈放在 channel 中,並廣播到整個網絡
func handleConn(conn net.Conn) { io.WriteString(conn, "Enter a new BPM:") scanner := bufio.NewScanner(conn) // take in BPM from stdin and add it to blockchain after // conducting necessary validation go func() { for scanner.Scan() { bpm, err := strconv.Atoi(scanner.Text()) if err != nil { log.Printf("%v not a number: %v", scanner.Text(), err) continue } newBlock, err := generateBlock( Blockchain[len(Blockchain)-1], bpm) if err != nil { log.Println(err) continue } if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) { newBlockchain := append(Blockchain, newBlock) replaceChain(newBlockchain) } bcServer <- Blockchain io.WriteString(conn, "\nEnter a new BPM:") } }() defer conn.Close() }
我們創建一個 scanner,並通過 for scanner.Scan()
來持續接收連接中發來的數據。為了簡化,我們把 BPM 數值轉化成字符串。bcServer <- Blockchain
是表示我們將新的鏈寫入 channel 中。
通過 TCP 鏈接將最新的鏈廣播出去時,我們需要:
- 將數據序列化成 JSON 格式
- 通過 timer 來定時廣播
- 在控制臺中打印出來,方便我們查看鏈的最新狀態
// simulate receiving broadcast go func() { for { time.Sleep(30 * time.Second) output, err := json.Marshal(Blockchain) if err != nil { log.Fatal(err) } io.WriteString(conn, string(output)) } }() for _ = range bcServer { spew.Dump(Blockchain) }
整個 handleConn
函數差不多就完成了,通過這裏[4]可以獲得完整的代碼。
有意思的地方
現在讓我們來啟動整個程序,go run main.go
就像我們預期的,首先創建了“創世塊”,接著啟動了 TCP server 並監聽9000端口。
接著我們打開一個新的終端,連接到那個端口。(我們用不同顏色來區分)nc localhost 9000
接下來我們輸入一個BPM值:
接著我們從第一個終端(節點)中能看到(依據輸入的BPM)創建了新的塊。
我們等待30秒後,可以從其他終端(節點)看到廣播過來的最新的鏈。
http://www.aibbt.com/a/18709.html
下一步
到目前為止,我們為這個例子添加了簡單的、本地模擬的網絡能力。當然,肯定有讀者覺得這不夠有說服力。但本質上來說,這就是區塊鏈的網絡層。它能接受外部數據並改變內在數據的狀態,又能將內在數據的最新狀態廣播出去。
接下來你需要學習的是一些主流的共識算法,比如 PoW (Proof-of-Work) 和 PoS (Proof-of-Stake) 等。當然,我們會繼續在後續的文章中將共識算法添加到這個例子中。
200行Go代碼實現自己的區塊鏈——區塊生成與網絡通信