1. 程式人生 > >200行Go代碼實現自己的區塊鏈——區塊生成與網絡通信

200行Go代碼實現自己的區塊鏈——區塊生成與網絡通信

type hash lazy avi present lte () cti 裏的

在第一篇文章[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 的值來創建塊,這裏會用到前面的函數:generateBlockisBlockValid,和 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代碼實現自己的區塊鏈——區塊生成與網絡通信