天天看點

Go語言golang 200行寫區塊鍊源代碼分析

Github上有一個Repo,是一個使用Go語言(golang),不到200行代碼些的區塊鍊源代碼,準确的說是174行。原作者起了個名字是 Code your own blockchain in less than 200 lines of Go! 而且作者也為此寫了一篇文章。 https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc

這篇文章是一個大概的思路和代碼的實作,當然還有很多代碼的邏輯沒有涉及,是以我就針對這不到200行的代碼進行一個分析,包含原文章裡沒有涉及到的知識點,對Go語言,區塊鍊都會有一個更深的認識。

所有的源代碼都在這裡: https://github.com/nosequeldeebee/blockchain-tutorial/blob/master/main.go

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/gorilla/mux"
	"github.com/joho/godotenv"
)
           

複制

在源代碼的開頭,是作者引入的一些包,有标準的,也有第三方的。像sha256,hex這些标準包是為了sha-256編碼用的,其他還有啟動http服務,列印日志的log,并發控制的sync,時間戳的time。

第三方包有三個,其中兩個我都詳細介紹過,相信大家不會陌生。

go-spew

是一個變量結構體的調試利器,可以列印出變量結構體對應的資料和結構,調試非常友善,詳細深入了解可以參考我這篇文章。Go語言經典庫使用分析(八)| 變量資料結構調試利器 go-spew

gorilla/mux

是一個web路由服務,可以很簡單的幫我們建構web服務。以前我深入分析過gorilla/handler,他們都是一家出産整體思路差不多,可以參考下。 Go語言經典庫使用分析(三)| Gorilla Handlers 詳細介紹 。

不過目前用gin的比較多,也推薦使用gin https://github.com/gin-gonic/gin。

godotenv

是一個讀取配置文章的庫,可以讓我們讀取

.env

格式的配置檔案,比如從配置檔案裡讀取IP、PORT等新。不過目前配置檔案還是推薦YAML和TOML,對應的第三方庫是:

gopkg.in/yaml.v21 https://github.com/BurntSushi/toml

TOML是我的最愛。關于Go工具包管理等知識可以參考我這篇 Go語言實戰筆記(一)| Go包管理

既然要寫一個區塊鍊,那麼肯定的有一個區塊的實體,我們通過golang的

struct

來實作。

// Block represents each 'item' in the blockchain
type Block struct {
	Index     int
	Timestamp string
	BPM       int
	Hash      string
	PrevHash  string
}
           

複制

Block

裡包含幾個字段:

  1. Index 就是Block的順序索引
  2. Timestamp是生成Block的時間戳
  3. BPM,作者說代表心率,每分鐘心跳數
  4. Hash是通過sha256生成的散列值,對整個Block資料的Hash
  5. PrevHash 上一個Block的Hash,這樣區塊才能連在一起構成區塊鍊

這段代碼涉及到的是

Struct

的隻是,具體可以參考我這篇文章Go語言實戰筆記(七)| Go 類型

有了區塊Block了,那麼區塊鍊就非常好辦了。

// Blockchain is a series of validated Blocks
var Blockchain []Block
           

複制

就是這麼簡單,一個Block數組就是一個區塊鍊。區塊鍊的構成關鍵在于Hash和PrevHash,通過他們一個個串聯起來,就是一串Block,也就是區塊鍊。因為互相之間通過Hash和PrevHash進行關聯,是以整個鍊很難被篡改,鍊越長被篡改的成本越大,因為要把整個鍊全部改掉才能完成篡改的目的,這樣的話,其他節點驗證這次篡改肯定是不能通過的。

既然關鍵點在于Hash,是以我們要先算出來一個Block的資料的Hash,也就是對Block裡的字段進行Hash,計算出一個唯一的Hash值。

// SHA256 hasing
func calculateHash(block Block) string {
	record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.BPM) + block.PrevHash
	h := sha256.New()
	h.Write([]byte(record))
	hashed := h.Sum(nil)
	return hex.EncodeToString(hashed)
}
           

複制

sha256

是golang内置的sha256的散列标準庫,可以讓我們很容易的生成對應資料的散列值。從源代碼看,是把

Block

的所有字段進行字元串拼接,然後通過

sha256

進行散列,散列的資料再通過

hex.EncodeToString

轉換為16進制的字元串,這樣就得到了我們常見的

sha256

散列值,類似這樣的字元串

8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92

Block的散列值被我們計算出來了,Block的Hash和PrevHash這兩個字段搞定了,那麼我們現在就可以生成一個區塊了,因為其他幾個字段都是可以自動生成的。

// create a new block using previous block's hash
func generateBlock(oldBlock Block, BPM int) Block {

	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
}
           

複制

因為區塊鍊是順序相連的,是以我們在生成一個新的區塊的時候,必須知道上一個區塊,也就是源代碼裡的

oldBlock

。另外一個參數

BPM

就是我們需要在區塊裡存儲的資料資訊了,這裡作者示範的例子是心率,我們可以換成其他業務中想要的資料。

Index

是上一個區塊的

Index+1

,保持順序;

Timestamp

通過

time.Now()

可以得到;Hash通過

calculateHash

方法計算出來。這樣我們就生成了一個新的區塊。

在這裡作者并沒有使用POW(工作量證明)這類算法來生成區塊,而是沒有任何條件的,這裡主要是為了模拟區塊的生成,示範友善。

區塊可以生成了,但是生成的區塊是否可信,我們還得對他進行校驗,不能随便生成一個區塊。在比特币(BitCoin)中校驗比較複雜,這裡作者采用了簡單模拟的方式。

// 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
}
           

複制

簡單的對比

Index

,

Hash

是否是正确的,并且重新計算了一遍

Hash

,防止被篡改。

到了這裡,關于區塊鍊的代碼已經全部完成了,剩下的就是把區塊鍊的生成、檢視等包裝成一個Web服務,可以通過API、浏覽器通路檢視。因為作者這裡沒有實作P2P網絡,是以采用的是WEB服務的方式。

// create handlers
func makeMuxRouter() http.Handler {
	muxRouter := mux.NewRouter()
	muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
	muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
	return muxRouter
}
           

複制

通過

mux

定義了兩個Handler,URL都是

/

,但是對應的

Method

是不一樣的。

GET

方法通過

handleGetBlockchain

函數實作,用于擷取區塊鍊的資訊。

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))
}
           

複制

Blockchain

是一個

[]Block

handleGetBlockchain

函數的作用是把

Blockchain

格式化為JSON字元串,然後顯示出來。

io.WriteString

是一個很好用的函數,可以往

Writer

裡寫入字元串。更多參考 Go語言實戰筆記(十九)| Go Writer 和 Reader

‘POST'方法通過

handleWriteBlock

函數實作,用于模拟區塊的生成。

func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	
	//使用了一個Mesage結構體,更友善的存儲BPM
	var msg Message

    //接收請求的資料資訊,類似{"BPM":60}這樣的格式
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&msg); err != nil {
		respondWithJSON(w, r, http.StatusBadRequest, r.Body)
		return
	}
	defer r.Body.Close()

    //控制并發,生成區塊鍊,并且校驗
	mutex.Lock()
	prevBlock := Blockchain[len(Blockchain)-1]
	newBlock := generateBlock(prevBlock, msg.BPM)

    //校驗區塊鍊
	if isBlockValid(newBlock, prevBlock) {
		Blockchain = append(Blockchain, newBlock)
		spew.Dump(Blockchain)
	}
	mutex.Unlock()

    //傳回新的區塊資訊
	respondWithJSON(w, r, http.StatusCreated, newBlock)

}
           

複制

以上代碼我進行了注釋,便于了解。主要是通過POST發送一個

{"BPM":60}

格式的BODY來添加區塊,如果格式正确,那麼就生成區塊進行校驗,合格了就加入到區塊裡;如果格式不對,那麼傳回錯誤資訊。

用于控制并發的鎖可以參考Go語言實戰筆記(十七)| Go 讀寫鎖

這個方法裡有個

Message

結構體,主要是為了便于操作友善。

// Message takes incoming JSON payload for writing heart rate
type Message struct {
	BPM int
}
           

複制

傳回的JSON資訊,也被抽取成了一個函數

respondWithJSON

,便于公用。

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的Handler已經好了,現在我們要啟動我們的Web服務了。

// web server
func run() error {
	mux := makeMuxRouter()
	//從配置檔案裡讀取監聽的端口
	httpPort := os.Getenv("PORT")
	log.Println("HTTP Server Listening on port :", httpPort)
	s := &http.Server{
		Addr:           ":" + httpPort,
		Handler:        mux,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}

	if err := s.ListenAndServe(); err != nil {
		return err
	}

	return nil
}
           

複制

和原生的

http.Server

基本一樣,應該比較好了解。mux其實也是一個Handler,這就是整個Handler處理鍊。現在我們就差一個

main

主函數來啟動我們整個程式了。

//控制并發的鎖
var mutex = &sync.Mutex{}

func main() {
    //加載env配置檔案
	err := godotenv.Load()
	if err != nil {
		log.Fatal(err)
	}

    //開啟一個goroutine生成一個創世區塊
	go func() {
		t := time.Now()
		genesisBlock := Block{}
		genesisBlock = Block{0, t.String(), 0, calculateHash(genesisBlock), ""}
		spew.Dump(genesisBlock)

		mutex.Lock()
		Blockchain = append(Blockchain, genesisBlock)
		mutex.Unlock()
	}()
	log.Fatal(run())

}
           

複制

整個

main

函數并不太複雜,主要就是加載env配置檔案,開啟一個go協程生成一個創世區塊并且添加到區塊鍊的第一個位置,然後就是通過

run

函數啟動Web服務。

然和一個區塊鍊都有一個創世區塊,也就是第一個區塊。有了第一個區塊我們才能添加第二個,第三個,第N個區塊。創世區塊因為是第一個區塊,是以它是沒有

PrevHash

的。

終于可以運作了,假設我們設定的PORT是8080,現在我們通過

go run main.go

啟動這個簡易的區塊鍊程式,就可以看到控制台輸出的創世區塊資訊。然後我們通過浏覽器打開

http://localhost:8080

也可以看到這個區塊鍊的資訊,裡面隻有一個創世區塊。

如果我們要新增一個區塊,通過curl或者postman,向

http://localhost:8080

發送body格式為

{"BPM":60}

的POST的資訊即可。然後在通過浏覽器通路

http://localhost:8080

檢視區塊鍊資訊,驗證是否已經添加成功。

到這裡,整個源代碼的分析已經完了,我們看下這個簡易的區塊鍊涉及到多少知識:

  1. sha256散列
  2. 位元組到16進制轉換
  3. 并發同步鎖
  4. Web服務
  5. 配置檔案
  6. 後向式連結清單
  7. 結構體
  8. JSON
  9. ……

等等,上面的很多知識,我已經在文章中講解或者通過以前些的文章說明,大家可以看一下,詳細了解。

區塊鍊作為一個新的分布式資料存儲技術,在追蹤,信用,防篡改等友善肯定可以發揮更大的作用,當然這也是一個不錯的機會,這裡推薦基本不錯的書籍,可以更好的了解和深入學習區塊鍊。

本文為原創文章,轉載注明出處,歡迎掃碼關注公衆号

flysnow_org

或者網站asf http://www.flysnow.org/