區塊鏈的原理與golang實現例子

liwan2018發表於2018-02-26

什麼是區塊鏈

區塊鏈有多火,就不用我介紹了,你能通過搜尋引擎跳轉到這裡,就證明你是區塊鏈的fan了。既然進來了,就不會讓你白來,no bb, 直接上乾貨!

(開場白,也不全是廢話)區塊鏈是 21 世紀最具革命性的技術之一,它仍然處於不斷成長的階段,而且還有很多潛力尚未顯現出來。 本質上,區塊鏈只是一個分散式資料庫而已。 不過,使它獨一無二的是,區塊鏈是一個公開的資料庫,而不是一個私人資料庫,也就是說,每個使用它的人都有一個完整或部分的副本。 只有經過其他資料庫管理員的同意,才能向資料庫中新增新的記錄。 此外,也正是由於區塊鏈,才使得加密貨幣和智慧合約成為現實。

綜而述之,用一個形象的比如:區塊鏈就是一個去中心化、分散式”記賬本”。

區塊鏈原理

1.區塊

讓我們從 “區塊鏈” 中的 “區塊” 談起。在區塊鏈中,儲存有效資訊的是區塊。
比如,比特幣區塊儲存的有效資訊,就是比特幣交易,交易資訊也是所有加密貨幣的本質。除此以外,區塊還包含了一些技術資訊,比如版本,當前時間戳和前一個區塊的雜湊。

這裡,我們並不會實現一個像比特幣技術規範所描述的區塊鏈,而是實現一個簡化版的區塊鏈,它僅包含了一些關鍵資訊。看起來就像是這樣:

type Block struct {
      Timestamp     int64
      Data          []byte
      PrevBlockHash []byte
      Hash          []byte
}
      - Timestamp 是當前時間戳,也就是區塊建立的時間。

      - Data 是區塊儲存的實際有效的資訊。

      -  PrevBlockHash 儲存的是前一個塊的雜湊。

      -  Hash 是當前塊的雜湊。

在比特幣技術規範中,Timestamp, PrevBlockHash, Hash 是區塊頭(block header),區塊頭是一個單獨的資料結構。而交易,也就是這裡的 Data, 是另一個單獨的資料結構。為了簡便起見,我把這兩個混合在了一起。

那麼,我們要如何計算雜湊呢?如何計算雜湊,是區塊鏈一個非常重要的部分。正是由於這個特性,才使得區塊鏈是安全的。計算一個雜湊,是在計算上非常困難的一個操作。即使在高速電腦上,也要花費不少時間 (這就是為什麼人們會購買 GPU 來挖比特幣) 。這是一個有意為之的架構設計,它故意使得加入新的區塊十分困難,因此可以保證區塊一旦被加入以後,就很難再進行修改。

目前,我們僅取了 Block 結構的一些欄位(Timestamp, Data 和 PrevBlockHash),並將它們相互連線起來,然後在連線後的結果上計算一個 SHA-256 的雜湊. 讓我們在 SetHash 方法中完成這個任務:

func (b *Block) SetHash() {
    timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
    headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
    hash := sha256.Sum256(headers)

    b.Hash = hash[:]
}

接下來,按照 Golang 的慣例,我們會實現一個用於簡化建立一個區塊的函式:

func NewBlock(data string, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}}
    block.SetHash()
    return block
}

這就是區塊部分的全部內容了!

2.鏈

下面讓我們來實現一個區塊鏈。本質上,區塊鏈僅僅是一個有著特定結構的資料庫,是一個有序,後向連線的列表。
這也就是說,區塊按照插入的順序進行儲存,每個塊都被連線到前一個塊。這樣的結構,能夠讓我們快速地獲取鏈上的最新塊,並且高效地通過雜湊來檢索一個塊。
在 Golang 中,可以通過一個 array 和 map 來實現這個結構:array 儲存有序的雜湊(Golang 中 array 是有序的),map 儲存 hask -> block 對(Golang 中, map 是無序的)。 但是在基本的原型階段,我們只用到了 array,因為現在還不需要通過雜湊來獲取塊。

type Blockchain struct {
    blocks []*Block
}

這就是我們的第一個區塊鏈!我從來沒有想過它會是這麼容易。

現在,讓我們能夠給它新增一個塊:

func (bc *Blockchain) AddBlock(data string) {
    prevBlock := bc.blocks[len(bc.blocks)-1]
    newBlock := NewBlock(data, prevBlock.Hash)
    bc.blocks = append(bc.blocks, newBlock)
}

完成!不過,真的就這樣了嗎?

為了加入一個新的塊,我們必須要有一個已有的塊,但是,現在我們的鏈是空的,一個塊都沒有!所以,在任何一個區塊鏈中,都必須至少有一個塊。這樣的塊,也就是鏈中的第一個塊,通常叫做創世塊(genesis block). 讓我們實現一個方法來建立一個創世塊:

func NewGenesisBlock() *Block {
    return NewBlock("Genesis Block", []byte{})
}

現在,我們可以實現一個函式來建立有創世塊的區塊鏈:

func NewBlockchain() *Blockchain {
    return &Blockchain{[]*Block{NewGenesisBlock()}}
}

來檢查一個我們的區塊鏈是否如期工作:

func main() {
    bc := NewBlockchain()

    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")
    for _, block := range bc.blocks {
        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        fmt.Println()
    }
}

一個完整的程式碼

package main

import (
    "crypto/sha256"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "log"
    "net/http"
    "sort"
    "strings"
    "time"
    "websocket"
    //"golang.org/x/net/websocket"
)

const (
    queryLatest = iota
    queryAll
    responseBlockchain
)

var genesisBlock = &Block{
    Index:        0,
    PreviousHash: "0",
    Timestamp:    1465154705,
    Data:         "my genesis block!!",
    Hash:         "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7",
}

var (
    sockets      []*websocket.Conn
    blockchain   = []*Block{genesisBlock}
    httpAddr     = flag.String("api", ":3001", "api server address.")
    p2pAddr      = flag.String("p2p", ":6001", "p2p server address.")
    initialPeers = flag.String("peers", "ws://localhost:6001", "initial peers")
)

type Block struct {
    Index        int64  `json:"index"`
    PreviousHash string `json:"previousHash"`
    Timestamp    int64  `json:"timestamp"`
    Data         string `json:"data"`
    Hash         string `json:"hash"`
}

func (b *Block) String() string {
    return fmt.Sprintf("index: %d,previousHash:%s,timestamp:%d,data:%s,hash:%s", b.Index, b.PreviousHash, b.Timestamp, b.Data, b.Hash)
}

type ByIndex []*Block

func (b ByIndex) Len() int           { return len(b) }
func (b ByIndex) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
func (b ByIndex) Less(i, j int) bool { return b[i].Index < b[j].Index }

type ResponseBlockchain struct {
    Type int    `json:"type"`
    Data string `json:"data"`
}

func errFatal(msg string, err error) {
    if err != nil {
        log.Fatalln(msg, err)
    }
}

func connectToPeers(peersAddr []string) {
    for _, peer := range peersAddr {
        if peer == "" {
            continue
        }
        ws, err := websocket.Dial(peer, "", peer)
        if err != nil {
            log.Println("dial to peer", err)
            continue
        }
        initConnection(ws)
    }
}
func initConnection(ws *websocket.Conn) {
    go wsHandleP2P(ws)

    log.Println("query latest block.")
    ws.Write(queryLatestMsg())
}

func handleBlocks(w http.ResponseWriter, r *http.Request) {
    bs, _ := json.Marshal(blockchain)
    w.Write(bs)
}
func handleMineBlock(w http.ResponseWriter, r *http.Request) {
    var v struct {
        Data string `json:"data"`
    }
    decoder := json.NewDecoder(r.Body)
    defer r.Body.Close()
    err := decoder.Decode(&v)
    if err != nil {
        w.WriteHeader(http.StatusGone)
        log.Println("[API] invalid block data : ", err.Error())
        w.Write([]byte("invalid block data. " + err.Error() + "\n"))
        return
    }
    block := generateNextBlock(v.Data)
    addBlock(block)
    broadcast(responseLatestMsg())
}

func handlePeers(w http.ResponseWriter, r *http.Request) {
    var slice []string
    for _, socket := range sockets {
        if socket.IsClientConn() {
            slice = append(slice, strings.Replace(socket.LocalAddr().String(), "ws://", "", 1))
        } else {
            slice = append(slice, socket.Request().RemoteAddr)
        }
    }
    bs, _ := json.Marshal(slice)
    w.Write(bs)
}

func handleAddPeer(w http.ResponseWriter, r *http.Request) {
    var v struct {
        Peer string `json:"peer"`
    }
    decoder := json.NewDecoder(r.Body)
    defer r.Body.Close()
    err := decoder.Decode(&v)
    if err != nil {
        w.WriteHeader(http.StatusGone)
        log.Println("[API] invalid peer data : ", err.Error())
        w.Write([]byte("invalid peer data. " + err.Error()))
        return
    }
    connectToPeers([]string{v.Peer})
}

func wsHandleP2P(ws *websocket.Conn) {
    var (
        v    = &ResponseBlockchain{}
        peer = ws.LocalAddr().String()
    )
    sockets = append(sockets, ws)

    for {
        var msg []byte
        err := websocket.Message.Receive(ws, &msg)
        if err == io.EOF {
            log.Printf("p2p Peer[%s] shutdown, remove it form peers pool.\n", peer)
            break
        }
        if err != nil {
            log.Println("Can't receive p2p msg from ", peer, err.Error())
            break
        }

        log.Printf("Received[from %s]: %s.\n", peer, msg)
        err = json.Unmarshal(msg, v)
        errFatal("invalid p2p msg", err)

        switch v.Type {
        case queryLatest:
            v.Type = responseBlockchain

            bs := responseLatestMsg()
            log.Printf("responseLatestMsg: %s\n", bs)
            ws.Write(bs)

        case queryAll:
            d, _ := json.Marshal(blockchain)
            v.Type = responseBlockchain
            v.Data = string(d)
            bs, _ := json.Marshal(v)
            log.Printf("responseChainMsg: %s\n", bs)
            ws.Write(bs)

        case responseBlockchain:
            handleBlockchainResponse([]byte(v.Data))
        }

    }
}

func getLatestBlock() (block *Block) { return blockchain[len(blockchain)-1] }
func responseLatestMsg() (bs []byte) {
    var v = &ResponseBlockchain{Type: responseBlockchain}
    d, _ := json.Marshal(blockchain[len(blockchain)-1:])
    v.Data = string(d)
    bs, _ = json.Marshal(v)
    return
}

func queryLatestMsg() []byte { return []byte(fmt.Sprintf("{\"type\": %d}", queryLatest)) }

func queryAllMsg() []byte    { return []byte(fmt.Sprintf("{\"type\": %d}", queryAll)) }

func calculateHashForBlock(b *Block) string {
    return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d%s%d%s", b.Index, b.PreviousHash, b.Timestamp, b.Data))))
}

func generateNextBlock(data string) (nb *Block) {
    var previousBlock = getLatestBlock()
    nb = &Block{
        Data:         data,
        PreviousHash: previousBlock.Hash,
        Index:        previousBlock.Index + 1,
        Timestamp:    time.Now().Unix(),
    }
    nb.Hash = calculateHashForBlock(nb)
    return
}

func addBlock(b *Block) {
    if isValidNewBlock(b, getLatestBlock()) {
        blockchain = append(blockchain, b)
    }
}

func isValidNewBlock(nb, pb *Block) (ok bool) {
    if nb.Hash == calculateHashForBlock(nb) &&
        pb.Index+1 == nb.Index &&
        pb.Hash == nb.PreviousHash {
        ok = true
    }
    return
}

func isValidChain(bc []*Block) bool {
    if bc[0].String() != genesisBlock.String() {
        log.Println("No same GenesisBlock.", bc[0].String())
        return false
    }
    var temp = []*Block{bc[0]}
    for i := 1; i < len(bc); i++ {
        if isValidNewBlock(bc[i], temp[i-1]) {
            temp = append(temp, bc[i])
        } else {
            return false
        }
    }
    return true
}

func replaceChain(bc []*Block) {
    if isValidChain(bc) && len(bc) > len(blockchain) {
        log.Println("Received blockchain is valid. Replacing current blockchain with received blockchain.")
        blockchain = bc
        broadcast(responseLatestMsg())
    } else {
        log.Println("Received blockchain invalid.")
    }
}
func broadcast(msg []byte) {
    for n, socket := range sockets {
        _, err := socket.Write(msg)
        if err != nil {
            log.Printf("peer [%s] disconnected.", socket.RemoteAddr().String())
            sockets = append(sockets[0:n], sockets[n+1:]...)
        }
    }
}

func handleBlockchainResponse(msg []byte) {
    var receivedBlocks = []*Block{}

    err := json.Unmarshal(msg, &receivedBlocks)
    errFatal("invalid blockchain", err)

    sort.Sort(ByIndex(receivedBlocks))

    latestBlockReceived := receivedBlocks[len(receivedBlocks)-1]
    latestBlockHeld := getLatestBlock()
    if latestBlockReceived.Index > latestBlockHeld.Index {
        log.Printf("blockchain possibly behind. We got: %d Peer got: %d", latestBlockHeld.Index, latestBlockReceived.Index)
        if latestBlockHeld.Hash == latestBlockReceived.PreviousHash {
            log.Println("We can append the received block to our chain.")
            blockchain = append(blockchain, latestBlockReceived)
        } else if len(receivedBlocks) == 1 {
            log.Println("We have to query the chain from our peer.")
            broadcast(queryAllMsg())
        } else {
            log.Println("Received blockchain is longer than current blockchain.")
            replaceChain(receivedBlocks)
        }
    } else {
        log.Println("received blockchain is not longer than current blockchain. Do nothing.")
    }
}

func main() {
    flag.Parse()
    connectToPeers(strings.Split(*initialPeers, ","))

    http.HandleFunc("/blocks", handleBlocks)
    http.HandleFunc("/mine_block", handleMineBlock)
    http.HandleFunc("/peers", handlePeers)
    http.HandleFunc("/add_peer", handleAddPeer)
    go func() {
        log.Println("Listen HTTP on", *httpAddr)
        errFatal("start api server", http.ListenAndServe(*httpAddr, nil))
    }()

    http.Handle("/", websocket.Handler(wsHandleP2P))
    log.Println("Listen P2P on ", *p2pAddr)
    errFatal("start p2p server", http.ListenAndServe(*p2pAddr, nil))
}

連結: https://github.com/kofj/naivechain/blob/master/main.go


多少人忙得連寫部落格的時間都沒有喲!

相關文章