用go編寫區塊鏈系列之3--持久化與命令列
在上一篇文章中我們構建了一個帶PoW挖礦功能的區塊鏈。我們這個區塊鏈已經很接近一個全功能的區塊鏈,但是它還缺少一些很重要的特性。這一章中我們將將實現將區塊鏈資料存入資料庫,並且編寫一個簡單的命令列介面去與區塊鏈進行互動。本質上區塊鏈是一個分散式資料庫,在這裡我們忽略“分散式”而只關注資料庫。
本文英文原文見https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
1 選擇資料庫
到現在為止,我們的區塊鏈系統中沒有資料庫,我們每次執行區塊鏈的時候資料儲存在記憶體中,這使得我們不能夠重用區塊鏈,也不能與別人分享我們的區塊鏈。我們需要一個數據庫。比特幣使用的是LevelDB資料庫來儲存資料,而我們將使用BoltDB。
BoltDB是一個純go語言實現的key/value的資料庫,它源於Howard Chu的LMDB專案。該專案的目標是提供一個簡單快速和可信的工程資料庫,它不需要像Postgress或MySQL一樣需要資料庫伺服器。它的特點是:
- 簡單
- 用go語言實現
- 嵌入式,不要部署伺服器
- 它允許我們自己設計資料結構
BoltDB是一個Key/Value資料庫,資料以鍵值對的形式儲存,其中的鍵值對儲存在buckets桶裡,buckets桶類似於資料庫裡面的表。所以為了取得資料,你需要知道資料所在的buket和它的key。
BoltDB中沒有資料型別,key和value都是都是以位元組陣列的形式儲存的。由於我們需要儲存go結構體,我們需要先把結構體序列化以儲存資料,需要反序列化位元組陣列以讀取資料。我們使用go內建的庫encoding/gob來實現序列化/反序列化。
使用下列命令安裝BoldDB:
go get -u github.com/boltdb/bolt/...
2 資料結構
我們可以參考比特幣來定義我們的資料結構。比特幣中使用了倆個"buckets"來儲存資料:
(1)blocks儲存區塊資料。
(2)chainstate儲存區塊鏈狀態資料。包括所有未花費的交易輸出和一些元資料。
區塊資料都是以一個個檔案的形式分開儲存。這是為了效能考慮:讀取一個區塊不需要把其它所有的區塊載入進記憶體。在blocks中,key/value對定義為:
'b' + 32-byte block hash -> block index record
'f' + 4-byte file number -> file information record
'l' -> 4-byte file number: the last block file number used
'R' -> 1-byte boolean: whether we're in the process of reindexing
'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
't' + 32-byte transaction hash -> transaction index recor
在chainstate中,key/value對定義為:
'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
由於我們還沒有引入交易,所以我們只需要有blocks桶。此外我們將所有資料儲存在一個檔案中,所以我們現在只需要定義倆個鍵值對:
32-byte block-hash -> Block structure (serialized)
'l' -> the hash of the last block in a chain
3 序列化
由於在BoltDB中所有資料都是以位元組陣列的形式儲存,所以我們使用encoding/gob庫來進行序列化。我們對block結構進行序列化的函式:
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
我們的反序列化函式:
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
4 持久化
我們從新建區塊函式NewBlockchain()
開始,它新建了一個blockchain例項並且將創世區塊新增進來。它的實現邏輯是:
(1)開啟一個數據庫檔案。
(2)檢查是否有區塊鏈存在裡面。
(3)如果有區塊鏈:
a 建立一個新的blockchain物件
b 設定blockchain物件的tip元素為最後一個區塊的hash
(4)如果沒有區塊鏈:
a 建立一個創世區塊
b 儲存創世區塊到資料庫
c 儲存創世區塊hash鍵值對 l->區塊hash
d 建立blockchain物件,它的tip指向創世區塊的hash
程式碼實現是:
func NewBlockchain() *Blockchain {
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
bc := Blockchain{tip, db}
return &bc
}
分析程式碼
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
這是BoltDB資料庫開啟資料庫的標準方式。
在BoltDB中對資料庫的交易都是一筆交易。我們在這裡以read-write方式對資料庫進行更新操作:
err = db.Update(func(tx *bolt.Tx) error {
...
})
這個函式的核心程式碼如下。
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
如果bucket存在,則我們讀取key為"l"的區塊hash。否則,我們建立創世區塊,然後建立名稱為blockBucket的bucket,並將以創世區塊hash為key的創世區塊資料儲存到bucket,將以l為key的創世區塊hash存入bucket。
最後建立區塊鏈:
bc := Blockchain{tip, db}
下面是區塊鏈結構體,它的db指向BoltDB資料庫,它的tip指向最新的區塊hash。
type Blockchain struct {
tip []byte
db *bolt.DB
}
我們要更新的另一個方法是AddBlock。
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
if err != nil {
log.Panic(err)
}
newBlock := NewBlock(data, lastHash)
err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return nil
})
}
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
這裡的View方法是使用BoltDB的只讀方法來讀取上一個區塊的hash儲存到lashHash變數。
newBlock := NewBlock(data, lastHash)
err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return nil
})
當挖出一個新區塊時,就使用Update方法將新區快儲存到bucket中,然後更新“l”鍵。
5 區塊鏈迭代器
我們定義一個BlockchainIterator結構體用來對區塊鏈進行迭代:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
這個迭代器儲存了最新的區塊Hash,所以它是採用反向迭代的方式,從最新的區塊向創世區塊的方向迭代區塊鏈,迭代方法如下,它根據當前的currentHash,從bucket中查詢到對應的區塊並返回,然後重新更新currentHash。
func (i *BlockchainIterator) Next() *Block {
var block *Block
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
encodedBlock := b.Get(i.currentHash)
block = DeserializeBlock(encodedBlock)
return nil
})
if(err != nil){
log.Panic(err)
}
i.currentHash = block.PrevBlockHash
return block
}
6 CLI
目前為止我們還不能通過命令列操作區塊鏈,到了該改進的額時候了!我們想實現這樣的命令列功能:
blockchain_go addblock -data "Pay 0.031337 for a coffee"
blockchain_go printchain
我們新建一個處理CLI的結構體CLI:
type CLI struct {
bc *Blockchain
}
它的入口函式是Run(),它使用標準的flag庫去處理命令列引數:
func (cli *CLI) Run() {
cli.validateArgs()
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
cli.printUsage()
os.Exit(1)
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
首先建立了倆個命令addblock和printchain,並且給addblock建立了一個data引數,data引數解析結果存在addBlockData中:
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
cli.printUsage()
os.Exit(1)
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
在這裡解析命令列,如果是addblock就呼叫addBlock(),如果是printchain就呼叫printChain()。這倆個處理函式在下面:
func (cli *CLI) addBlock(data string) {
cli.bc.AddBlock(data)
fmt.Println("Success!")
}
func (cli *CLI) printChain() {
bci := cli.bc.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevBlockHash) == 0 {
break
}
}
}
7 執行
現在組裝main函式,:
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
所有程式碼見https://github.com/liuzhijun23/CliBlockchain
執行看下結果:
F:\GoPro\src\TBCPro\database and cli>go_build_TBCPro_database_and_cli.exe printc
hain
Mining the block containing "Genesis Block"
00000093fec9885d88da8a7ae9e90256f8e5583c0b0d9dc09e142ee7fbe1c9f9
5886574
Prev. hash:
Data: Genesis Block
Hash: 00000093fec9885d88da8a7ae9e90256f8e5583c0b0d9dc09e142ee7fbe1c9f9
PoW: true
F:\GoPro\src\TBCPro\database and cli>go_build_TBCPro_database_and_cli.exe addblo
ck -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000b423e1a0ebed5d3232ede271bce9116e0358983d2fb8881d5a2f21479b
12535093
Success!
F:\GoPro\src\TBCPro\database and cli>go_build_TBCPro_database_and_cli.exe addblo
ck -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000ef17dff45ba2fecbadb3a5e61c37690cdeca14741877eedb6ca9437bb1
26720742
Success!
F:\GoPro\src\TBCPro\database and cli>go_build_TBCPro_database_and_cli.exe printc
hain
Prev. hash: 000000b423e1a0ebed5d3232ede271bce9116e0358983d2fb8881d5a2f21479b
Data: Pay 0.31337 BTC for a coffee
Hash: 000000ef17dff45ba2fecbadb3a5e61c37690cdeca14741877eedb6ca9437bb1
PoW: true
Prev. hash: 00000093fec9885d88da8a7ae9e90256f8e5583c0b0d9dc09e142ee7fbe1c9f9
Data: Send 1 BTC to Ivan
Hash: 000000b423e1a0ebed5d3232ede271bce9116e0358983d2fb8881d5a2f21479b
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 00000093fec9885d88da8a7ae9e90256f8e5583c0b0d9dc09e142ee7fbe1c9f9
PoW: true
8 結論
下一節將構建帶有地址、錢包和交易的區塊鏈,不要走開哦!