在第一篇文章[1]中,我們向大家展示瞭如何通過精煉的Go代碼實現一個簡單的區塊鏈。如何計算每個塊的 Hash 值,如何驗證塊數據,如何讓塊鏈接起來等等,但是所有這些都是跑在一個節點上的。文章發佈後,讀者反響熱烈,紛紛留言讓我快點填坑(網絡部分),於是就誕生了這第二篇文章。
這篇文章在之前的基礎上,解決多個節點網絡內,如何生成塊、如何通信、如何廣播消息等。
流程
第一個節點創建“創始區塊”,同時啟動 TCP server並監聽一個端口,等待其他節點連接。
Step 1
啟動其他節點,並與第一個節點建立TCP連接(這裡我們通過不同的終端來模擬其他節點)
創建新的塊
Step 2
第一個節點驗證新生成塊
驗證之後廣播(鏈的新狀態)給其他節點
Step 3
所有的節點都同步了最新的鏈的狀態
之後你可以重複上面的步驟,使得每個節點都創建TCP server並監聽(不同的)端口以便其他節點來連接。通過這樣的流程你將建立一個簡化的模擬的(本地的)P2P網絡,當然你也可以將節點的代碼編譯後,將二進制程序部署到雲端。
開始coding吧
設置與導入依賴
參考之前第一篇文章,我們使用相同的計算 hash 的函數、驗證塊數據的函數等。
設置
在工程的根目錄創建一個 <code>.env /<code>文件,並添加配置:
<code>ADDR=9000/<code>
通過 <code>go-spew /<code>包將鏈數據輸出到控制檯,方便我們閱讀:
<code>go get github.com/davecgh/go-spew/spew/<code>
通過 <code>godotenv /<code>包來加載配置文件:
<code>go get github.com/joho/godotenv/<code>
之後創建 <code>main.go /<code>文件。
導入
接著我們導入所有的依賴:
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 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 handles incoming concurrent Blocks
var bcServer chan Block
注:Channel 是 Go 語言中很重要的特性之一,它使得我們以流的方式讀寫數據,特別是用於併發編程。通過這裡[2]可以更深入地學習 Channel。
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
for {
conn, err := server.Accept
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
通過這個無限循環,我們可以接受其他節點的 TCP 鏈接,同時通過 <code>go handleConn(conn) /<code>啟動一個新的 go routine(譯者注:Rob Pike 不認為go routine 是協程,因此沒有譯為協程)來處理請求。
接下來是“處理請求”這個重要函數,其他節點可以創建新的塊並通過 TCP 連接發送出來。在這裡我們依然像第一篇文章一樣,以 BPM 來作為示例數據。
客戶端通過 <code>stdin /<code>輸入 BPM
以 BPM 的值來創建塊,這裡會用到前面的函數:<code>generateBlock/<code>,<code>isBlockValid/<code>,和<code>replaceChain/<code>
將新的鏈放在 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 io.WriteString(conn, "\nEnter a new BPM:")
}
}
defer conn.Close
}
我們創建一個 scanner,並通過 <code>for scanner.Scan /<code>來持續接收連接中發來的數據。為了簡化,我們把 BPM 數值轉化成字符串。<code>bcServer 是表示我們將新的鏈寫入 channel 中。/<code>
通過 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)
}
整個 <code>handleConn /<code>函數差不多就完成了,通過這裡[4]可以獲得完整的代碼。
有意思的地方
現在讓我們來啟動整個程序,
<code>go run main.go/<code>
就像我們預期的,首先創建了“創世塊”,接著啟動了 TCP server 並監聽9000端口。
接著我們打開一個新的終端,連接到那個端口。(我們用不同顏色來區分)
<code>nc localhost 9000/<code>
接下來我們輸入一個BPM值:
接著我們從第一個終端(節點)中能看到(依據輸入的BPM)創建了新的塊。
我們等待30秒後,可以從其他終端(節點)看到廣播過來的最新的鏈。
下一步
到目前為止,我們為這個例子添加了簡單的、本地模擬的網絡能力。當然,肯定有讀者覺得這不夠有說服力。但本質上來說,這就是區塊鏈的網絡層。它能接受外部數據並改變內在數據的狀態,又能將內在數據的最新狀態廣播出去。
接下來你需要學習的是一些主流的共識算法,比如 PoW (Proof-of-Work) 和 PoS (Proof-of-Stake) 等。當然,我們會繼續在後續的文章中將共識算法添加到這個例子中。
下一篇文章再見!
參考鏈接
[1] https://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=2653549361&idx=1&sn=019f54713891cf33ef3bef3b24773a96&chksm=813a62a9b64debbfdd24a8507bb974048a4456e5b0a2d5f685fb3bdf40366a25764c5df8afec&scene=21
[2] https://golangbot.com/channels/
[3] https://blog.golang.org/defer-panic-and-recover
[4] https://github.com/mycoralhealth/blockchain-tutorial/blob/master/networking/main.go