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
裡包含幾個字段:
- Index 就是Block的順序索引
- Timestamp是生成Block的時間戳
- BPM,作者說代表心率,每分鐘心跳數
- Hash是通過sha256生成的散列值,對整個Block資料的Hash
- 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
檢視區塊鍊資訊,驗證是否已經添加成功。
到這裡,整個源代碼的分析已經完了,我們看下這個簡易的區塊鍊涉及到多少知識:
- sha256散列
- 位元組到16進制轉換
- 并發同步鎖
- Web服務
- 配置檔案
- 後向式連結清單
- 結構體
- JSON
- ……
等等,上面的很多知識,我已經在文章中講解或者通過以前些的文章說明,大家可以看一下,詳細了解。
區塊鍊作為一個新的分布式資料存儲技術,在追蹤,信用,防篡改等友善肯定可以發揮更大的作用,當然這也是一個不錯的機會,這裡推薦基本不錯的書籍,可以更好的了解和深入學習區塊鍊。
本文為原創文章,轉載注明出處,歡迎掃碼關注公衆号 flysnow_org
或者網站asf http://www.flysnow.org/