寫一個最簡單的區塊鏈——Yet another Go tutorial
前言
為什麼說是最簡單的區塊鏈呢,因為根本寫不出一個完整的區塊鏈,甚至連區塊鏈的Demo都算不上。本文充其量可以當做Go語言的一個入門教程,至少對我來說是這樣。所以,即使讀者沒有任何區塊鏈和Go語言的知識,也可以放心往下看。
本文使用Go語言實現了
- 區塊的定義和構建
- 區塊鏈的定義和構建
- 新增交易
- 檢視區塊鏈內容
- 提供Go API和Web API兩種方式
區塊鏈的概念
區塊鏈源自比特幣,當年中本聰計劃打造一個完全去中心化的電子貨幣交易系統,區塊鏈應運而生。發明區塊鏈的動機,大概是中本聰覺得,任何中心化的系統都不夠安全,一旦把特權賦予某些人,就存在濫用職權和腐敗的可能。只有在去中心化的系統中,才會存在絕對的安全。
去中心化其實很簡單,直接讓每一個節點都儲存完整的交易資訊,自然就不需要中心節點了。但這樣會造成資源的極大浪費,也會造成通訊的擁堵。不過比特幣似乎就是這樣乾的,畢竟比特幣的總量不算很多,交易量也不至於太大。
去中心化的另一個問題是需要共識機制。因為區塊鏈是一個單向連結串列,如果兩個節點同時想要向連結串列頭部新增元素,勢必造成連結串列的分叉。共識機制規定了整個網路中最長的那個鏈為有效鏈,任何節點一旦發現其它鏈比本地儲存的鏈更長,就必須更換為那個最長的鏈。
此外,節點是不能隨意向區塊鏈中新增元素的,否則誰新增的最快,誰的區塊鏈就最長,那豈不是成了速度競賽。制約的方法是,一個節點產生的交易,必須由另一個節點記賬,才能加入區塊鏈中。為了鼓勵人們積極為其他人記賬,中本聰設計了“工作量證明”這一環節,也就是我們俗稱的“挖礦”。成功為他人記賬的節點,會收到若干個比特幣的獎勵。這樣一來,人們蜂擁而至,爭先為別人記賬,但交易數量有限,多個人同時記賬只會有一個人記賬成功,其他人無功而返。為了使這一過程不受網速的影響,使大家能夠公平競爭,中本聰規定,記賬者必須得到若干個0開頭的賬單摘要才算記賬成功。所謂賬單摘要,就是把賬單資料按照規定的組合方式,加上隨機數,再經過某種雜湊演算法(比如SHA256)得到的固定長度的資料串。目前的規定是以18個0開頭,每個0是一個16進位制數字,也就相當於平均每嘗試16 18 次才可能得到一個符合條件的賬單摘要。這也是為什麼挖礦需要消耗大量的計算資源,而且挖礦會越來越難(數字0開頭的數量還會進一步增多)。
說了這麼多理論,也該開始實踐了。但說著容易做著難,我們沒法把上面提到的所有特性都實現出來,只能實現最基礎的功能。即使如此,對幫助大家直觀地瞭解區塊鏈也已經足夠了。
Go語言實現區塊鏈
如果手邊有一臺電腦,建議按照本節的流程親自敲一遍程式碼。
0.配置開發環境
本來想把如何配置開發環境寫一寫,但實在太繁瑣,又難以滿足不同系統使用者的需求,遂作罷。大家只能發揮自己的聰明才智搞一搞了。
1. 定義區塊
// file: Block.go package core import ( "crypto/sha256" "encoding/hex" "time" ) type Block struct { // Block header Index int64 Timestamp int64 PrevBlockHash string Hash string // Block data Data string } func createBlock(prevBlock Block, data string) Block{ newBlock := Block{} newBlock.Index = prevBlock.Index + 1 newBlock.Timestamp = time.Now().Unix() newBlock.PrevBlockHash = prevBlock.Hash newBlock.Data = data newBlock.Hash = calculateHash(newBlock) return newBlock } func calculateHash(block Block) string { toBeHashed := string(block.Index) + string(block.Timestamp) + block.PrevBlockHash + block.Data hashInBytes := sha256.Sum256([]byte(toBeHashed)) return hex.EncodeToString(hashInBytes[:]) }
這段程式碼定義了一個結構體 Block
,用來表示一個區塊。區塊包含區塊頭和資料兩部分,其中,區塊頭包括序號 Index
、時間戳 Timestamp
、上一區塊的摘要 PrevBlockHash
以及當前區塊的摘要 Hash
。資料為了簡單起見,直接用 string
型別表示。
下面兩個私有函式分別用來建立區塊和計算給定區塊的摘要。之所以稱為私有函式,是因為函式名以小寫字母開頭,Go編譯器自動按照函式名首字母的大小寫決定該函式的訪問級別。在 calculateHash
函式中,我們把序號、時間戳、上一區塊的摘要以及資料連線成一個長字串,並計算該字串的雜湊值,作為當前區塊的摘要。需要注意,Go語言中宣告變數可以不顯式指定型別,但需要用 :=
符號來初始化。
2. 定義區塊鏈
// file: BlockChain.go package core import "fmt" type BlockChain struct { Blocks []*Block } func CreateBlockChain() BlockChain { genesisBlock := createBlock(Block{Index:-1}, "I am genesis block.") blockChain := BlockChain{} blockChain.Blocks = append(blockChain.Blocks, &genesisBlock) return blockChain } func (blockChain *BlockChain) AddTransaction(data string){ block := createBlock(*blockChain.Blocks[len(blockChain.Blocks) - 1], data) blockChain.Blocks = append(blockChain.Blocks, █) } func (blockChain *BlockChain) Print(){ for _, block := range blockChain.Blocks { fmt.Printf("Index: %d\n", block.Index) fmt.Printf("Timestamp: %d\n", block.Timestamp) fmt.Printf("PreBlockHash: %s\n", block.PrevBlockHash) fmt.Printf("Hash: %s\n", block.Hash) fmt.Printf("Data: %s\n", block.Data) fmt.Println() } }
這裡,我們把區塊鏈定義為另一個結構體,內部包含區塊的陣列切片。Go語言中的陣列切片,其實就是變長陣列,它的容量依賴於實際陣列的長度。
我們提供了三個公有函式。 CreateBlockChain
用來建立一個新的區塊鏈,在該函式中自動建立了一個區塊,稱為“創世區塊”,該區塊不含任何資料,Index為0,只用於標識區塊鏈的起點。 AddTransaction
函式用來新增交易,內部會建立一個新的區塊,並連結到區塊鏈上。 Print
函式用來列印完整的區塊鏈資訊。
細心的話可以發現,這裡的函式名前面增加了一些內容。在Go語言中,這種函式稱為方法,方法名前面的部分是接收者,類似於C++中的this指標。 AddTransaction
和 Print
方法都聲明瞭 BlockChain
型別的接收者,於是這兩個方法可以當做 BlockChain
型別的成員函式來使用。
3. Go API Demo
以上已經實現了區塊鏈的核心功能。我們現在寫一個demo來測試一下效果。
// file: main.go package main import ( "BlockChainDemo/core" ) func main(){ blockChain := core.CreateBlockChain() blockChain.AddTransaction("Send 1 BTC to Faye.") blockChain.AddTransaction("Send 2 BTC to Liling.") blockChain.Print() }
Go語言的主函式必須位於 main
包中,否則不能執行。執行該程式,輸出結果為
Index: 0 Timestamp: 1538731505 PreBlockHash: Hash: f8970ec722193096998452516c709c1890323e5df5bd7cf1e139b9c592394f6d Data: I am genesis block. Index: 1 Timestamp: 1538731505 PreBlockHash: f8970ec722193096998452516c709c1890323e5df5bd7cf1e139b9c592394f6d Hash: 12f8ca6f66b0b21e6d1ed7682265f849f46e4a4ff10e6ba794021eb25d0ab033 Data: Send 1 BTC to Faye. Index: 2 Timestamp: 1538731505 PreBlockHash: 12f8ca6f66b0b21e6d1ed7682265f849f46e4a4ff10e6ba794021eb25d0ab033 Hash: 1c849f10a0da4f2aa45275f1585ddc700fa46c24cb8162484bf628a16aeac5a0 Data: Send 2 BTC to Liling.
可以看到,新增兩次交易後,區塊鏈的長度變為3,除了創世節點,後面每個節點表示一次交易。每個區塊的 Hash
值與當前區塊和上一區塊都有關,因此任何人都無法篡改歷史資料,任何微小的改動都會導致後面鏈條中所有資料的變化。
4. Web API Demo
最後一部分,發揮Go語言強大的Web程式設計能力,我們提供一個Web API供HTTP訪問使用。
// file: server.go package main import ( "BlockChainDemo/core" "encoding/json" "io" "net/http" ) var blockChain core.BlockChain func run(){ http.HandleFunc("/blockchain/get", blockchainGetHandler) http.HandleFunc("/blockchain/write", blockchainWriteHandler) http.ListenAndServe("localhost:8888", nil) } func blockchainGetHandler(writer http.ResponseWriter, request *http.Request) { bytes, error := json.MarshalIndent(blockChain, "", "\t") if error != nil { http.Error(writer, error.Error(), http.StatusInternalServerError) return } io.WriteString(writer, string(bytes)) } func blockchainWriteHandler(writer http.ResponseWriter, request *http.Request){ data := request.URL.Query().Get("data") blockChain.AddTransaction(data) blockchainGetHandler(writer, request) } func main(){ blockChain = core.CreateBlockChain() run() }
實現方法非常簡單,設定兩個回撥函式,對應兩個URL,一個用來查詢當前區塊鏈,另一個用來向區塊鏈中新增交易。執行此程式,然後開啟瀏覽器,訪問 ofollow,noindex">http://localhost:8888/blockchain/get ,可以看到如下結果

get
此時只有創世區塊。接下來新增一個交易
http://localhost:8888/blockchain/write?data=Send 1 BTC to Faye.現在區塊鏈變成了這樣

write
每呼叫一次,區塊鏈中就新增一個區塊。
好了,到此為止,我們已經實現了預定的全部功能。
完整程式碼已上傳GitHub,請點選 jingedawang/BlockChainDemo 下載。
總結
區塊鏈並沒有想象中那麼神祕,也不是無所不能。最初版本的區塊鏈有很大侷限性,所以才有了區塊鏈2.0、3.0,以及以太坊、智慧合約的出現。想真正瞭解區塊鏈,不深入到行業應用中是不行的,所以本文只是淺嘗輒止,拋磚引玉而已。鑑於當下區塊鏈工程師年薪百萬,我想,是時候考慮轉行了:-)
參考資料
區塊鏈技術核心概念與原理講解 慕課網
用GO語言構建自己的區塊鏈 慕課網
Go 語言教程 菜鳥教程
golang 函式以及函式和方法的區別 D_Guco