引言
到目前為止,我們已經建構了一個有工作量證明機制的區塊鍊。有了工作量證明,挖礦也就有了着落。雖然目前的實作離一個有着完整功能的區塊鍊越來越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我們會将區塊鍊持久化到一個資料庫中,然後會提供一個簡單的指令行接口,用來完成一些與區塊鍊的互動操作。本質上,區塊鍊是一個分布式資料庫,不過,我們暫時先忽略 “分布式” 這個部分,僅專注于 “存儲” 這一點。
選擇資料庫
目前,我們的區塊鍊實作裡面并沒有用到資料庫,而是在每次運作程式時,簡單地将區塊鍊存儲在記憶體中。那麼一旦程式退出,所有的内容就都消失了。我們沒有辦法再次使用這條鍊,也沒有辦法與其他人共享,是以我們需要把它存儲到磁盤上。
那麼,我們要用哪個資料庫呢?實際上,任何一個資料庫都可以。在 比特币原始論文 中,并沒有提到要使用哪一個具體的資料庫,它完全取決于開發者如何選擇。 Bitcoin Core ,最初由中本聰釋出,現在是比特币的一個參考實作,它使用的是 LevelDB。而我們将要使用的是…
BoltDB
因為它:
- 非常簡單和簡約
- 用 Go 實作
- 不需要運作一個伺服器
- 能夠允許我們構造想要的資料結構
BoltDB GitHub 上的 README 是這麼說的:
Bolt 是一個純鍵值存儲的 Go 資料庫,啟發自 Howard Chu 的 LMDB. 它旨在為那些無須一個像 Postgres 和 MySQL 這樣有着完整資料庫伺服器的項目,提供一個簡單,快速和可靠的資料庫。
由于 Bolt 意在用于提供一些底層功能,簡潔便成為其關鍵所在。它的
API 并不多,并且僅關注值的擷取和設定。僅此而已。
聽起來跟我們的需求完美契合!來快速過一下:
Bolt 使用鍵值存儲,這意味着它沒有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,沒有行和列。相反,資料被存儲為鍵值對(key-value pair,就像 Golang 的 map)。鍵值對被存儲在 bucket 中,這是為了将相似的鍵值對進行分組(類似 RDBMS 中的表格)。是以,為了擷取一個值,你需要知道一個 bucket 和一個鍵(key)。
需要注意的一個事情是,Bolt 資料庫沒有資料類型:鍵和值都是位元組數組(byte array)。鑒于需要在裡面存儲 Go 的結構(準确來說,也就是存儲(塊)Block),我們需要對它們進行序列化,也就說,實作一個從 Go struct 轉換到一個 byte array 的機制,同時還可以從一個 byte array 再轉換回 Go struct。雖然我們将會使用 encoding/gob 來完成這一目标,但實際上也可以選擇使用 JSON, XML, Protocol Buffers 等等。之是以選擇使用 encoding/gob, 是因為它很簡單,而且是 Go 标準庫的一部分。
資料庫結構
在開始實作持久化的邏輯之前,我們首先需要決定到底要如何在資料庫中進行存儲。為此,我們可以參考 Bitcoin Core 的做法:
簡單來說,Bitcoin Core 使用兩個 “bucket” 來存儲資料:
- 其中一個 bucket 是 blocks,它存儲了描述一條鍊中所有塊的中繼資料
- 另一個 bucket 是 chainstate,存儲了一條鍊的狀态,也就是目前所有的未花費的交易輸出,和一些中繼資料
此外,出于性能的考慮,Bitcoin Core 将每個區塊(block)存儲為磁盤上的不同檔案。如此一來,就不需要僅僅為了讀取一個單一的塊而将所有(或者部分)的塊都加載到記憶體中。但是,為了簡單起見,我們并不會實作這一點。
在 blocks 中,key -> value 為:
key | value |
---|---|
+ 32 位元組的 block hash | block index record |
+ 4 位元組的 file number | file information record |
| the last block file number used |
+ 1 位元組的 boolean | 是否正在 reindex |
+ 1 位元組的 flag name length + flag name string | 1 byte boolean: various flags that can be on or off |
+ 32 位元組的 transaction hash | transaction index record |
在 chainstate,key -> value 為:
| unspent transaction output record for that transaction |
| 32 位元組的 block hash: the block hash up to which the database represents the unspent transaction outputs |
詳情可見 這裡。
因為目前還沒有交易,是以我們隻需要 blocks bucket。另外,正如上面提到的,我們會将整個資料庫存儲為單個檔案,而不是将區塊存儲在不同的檔案中。是以,我們也不會需要檔案編号(file number)相關的東西。最終,我們會用到的鍵值對有:
- 32 位元組的 block-hash -> block 結構
-
-> 鍊中最後一個塊的 hashl
這就是實作持久化機制所有需要了解的内容了。
序列化
上面提到,在 BoltDB 中,值隻能是
[]byte
類型,但是我們想要存儲
Block
結構。是以,我們需要使用 encoding/gob 來對這些結構進行序列化。
讓我們來實作
Block
的
Serialize
方法(為了簡潔起見,此處略去了錯誤處理):
func (b *Block) Serialize() []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) err := encoder.Encode(b) return result.Bytes() }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
這個部分比較直覺:首先,我們定義一個 buffer 存儲序列化之後的資料。然後,我們初始化一個
gob
encoder 并對 block 進行編碼,結果作為一個位元組數組傳回。
接下來,我們需要一個解序列化的函數,它會接受一個位元組數組作為輸入,并傳回一個
Block
. 它不是一個方法(method),而是一個單獨的函數(function):
func DeserializeBlock(d []byte) *Block { var block Block decoder := gob.NewDecoder(bytes.NewReader(d)) err := decoder.Decode(&block) return &block }
這就是序列化部分的内容了。
持久化
讓我們從
NewBlockchain
函數開始。在之前的實作中,它會建立一個新的
Blockchain
執行個體,并向其中加入創世塊。而現在,我們希望它做的事情有:
- 打開一個資料庫檔案
- 檢查檔案裡面是否已經存儲了一個區塊鍊
- 如果已經存儲了一個區塊鍊:
- 建立一個新的
執行個體Blockchain
- 設定
執行個體的 tip 為資料庫中存儲的最後一個塊的哈希Blockchain
- 建立一個新的
- 如果沒有區塊鍊:
- 建立創世塊
- 存儲到資料庫
- 将創世塊哈希儲存為最後一個塊的哈希
-
執行個體,其 tip 指向創世塊(tip 有尾部,尖端的意思,在這裡 tip 存儲的是最後一個塊的哈希)Blockchain
代碼大概是這樣:
func NewBlockchain() *Blockchain { var tip []byte db, err := bolt.Open(dbFile, 0600, nil) err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) if b == nil { genesis := NewGenesisBlock() b, err := tx.CreateBucket([]byte(blocksBucket)) 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 }
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
來一段一段地看下代碼:
db, err := bolt.Open(dbFile, 0600, nil)
這是打開一個 BoltDB 檔案的标準做法。注意,即使不存在這樣的檔案,它也不會傳回錯誤。
err = db.Update(func(tx *bolt.Tx) error { ... })
在 BoltDB 中,資料庫操作通過一個事務(transaction)進行操作。有兩種類型的事務:隻讀(read-only)和讀寫(read-write)。這裡,打開的是一個讀寫事務(
db.Update(...)
),因為我們可能會向資料庫中添加創世塊。
b := tx.Bucket([]byte(blocksBucket)) if b == nil { genesis := NewGenesisBlock() b, err := tx.CreateBucket([]byte(blocksBucket)) err = b.Put(genesis.Hash, genesis.Serialize()) err = b.Put([]byte("l"), genesis.Hash) tip = genesis.Hash } else { tip = b.Get([]byte("l")) }
這裡是函數的核心。在這裡,我們先擷取了存儲區塊的 bucket:如果存在,就從中讀取
l
鍵;如果不存在,就生成創世塊,建立 bucket,并将區塊儲存到裡面,然後更新
l
鍵以存儲鍊中最後一個塊的哈希。
另外,注意建立
Blockchain
一個新的方式:
bc := Blockchain{tip, db}
這次,我們不在裡面存儲所有的區塊了,而是僅存儲區塊鍊的 tip。另外,我們存儲了一個資料庫連接配接。因為我們想要一旦打開它的話,就讓它一直運作,直到程式運作結束。是以,
Blockchain
的結構現在看起來是這樣:
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 }) 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()) 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 })
這是 BoltDB 事務的另一個類型(隻讀)。在這裡,我們會從資料庫中擷取最後一個塊的哈希,然後用它來挖出一個新的塊的哈希:
newBlock := NewBlock(data, lastHash) b := tx.Bucket([]byte(blocksBucket)) err := b.Put(newBlock.Hash, newBlock.Serialize()) err = b.Put([]byte("l"), newBlock.Hash) bc.tip = newBlock.Hash
檢查區塊鍊
現在,産生的所有塊都會被儲存到一個資料庫裡面,是以我們可以重新打開一個鍊,然後向裡面加入新塊。但是在實作這一點後,我們失去了之前一個非常好的特性:我們再也無法列印區塊鍊的區塊了,因為現在不是将區塊存儲在一個數組,而是放到了資料庫裡面。讓我們來解決這個問題!
BoltDB 允許對一個 bucket 裡面的所有 key 進行疊代,但是所有的 key 都以位元組序進行存儲,而且我們想要以區塊能夠進入區塊鍊中的順序進行列印。此外,因為我們不想将所有的塊都加載到記憶體中(因為我們的區塊鍊資料庫可能很大!或者現在可以假裝它可能很大),我們将會一個一個地讀取它們。故而,我們需要一個區塊鍊疊代器(BlockchainIterator):
type BlockchainIterator struct { currentHash []byte db *bolt.DB }
每當要對鍊中的塊進行疊代時,我們就會建立一個疊代器,裡面存儲了目前疊代的塊哈希(currentHash)和資料庫的連接配接(db)。通過
db
,疊代器邏輯上被附屬到一個區塊鍊上(這裡的區塊鍊指的是存儲了一個資料庫連接配接的
Blockchain
執行個體),并且通過
Blockchain
方法進行建立:
func (bc *Blockchain) Iterator() *BlockchainIterator { bci := &BlockchainIterator{bc.tip, bc.db} return bci }
注意,疊代器的初始狀态為鍊中的 tip,是以區塊将從頭到尾,也就是從最新的到最舊的進行擷取。實際上,選擇一個 tip 就是意味着給一條鍊“投票”。一條鍊可能有多個分支,最長的那條鍊會被認為是主分支。在獲得一個 tip (可以是鍊中的任意一個塊)之後,我們就可以重新構造整條鍊,找到它的長度和需要建構它的工作。這同樣也意味着,一個 tip 也就是區塊鍊的一種辨別符。
BlockchainIterator
隻會做一件事情:傳回鍊中的下一個塊。
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 }) i.currentHash = block.PrevBlockHash return block }
這就是資料庫部分的内容了!
CLI
到目前為止,我們的實作還沒有提供一個與程式互動的接口:目前隻是在
main
函數中簡單執行了
NewBlockchain
和
bc.AddBlock
。是時候改變了!現在我們想要擁有這些指令:
blockchain_go addblock "Pay 0.031337 for a coffee" blockchain_go printchain
所有指令行相關的操作都會通過
CLI
結構進行處理:
type CLI struct { bc *Blockchain }
它的 “入口” 是
Run
函數:
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:]) case "printchain": err := printChainCmd.Parse(os.Args[2:]) default: cli.printUsage() os.Exit(1) } if addBlockCmd.Parsed() { if *addBlockData == "" { addBlockCmd.Usage() os.Exit(1) } cli.addBlock(*addBlockData) } if printChainCmd.Parsed() { cli.printChain() } }
- 25
- 26
- 27
- 28
- 29
- 30
我們會使用标準庫裡面的 flag 包來解析指令行參數:
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) addBlockData := addBlockCmd.String("data", "", "Block data")
首先,我們建立兩個子指令:
addblock
printchain
, 然後給
addblock
添加
-data
标志。
printchain
沒有任何标志。
switch os.Args[1] { case "addblock": err := addBlockCmd.Parse(os.Args[2:]) case "printchain": err := printChainCmd.Parse(os.Args[2:]) default: cli.printUsage() os.Exit(1) }
然後,我們檢查使用者提供的指令,解析相關的
flag
子指令:
if addBlockCmd.Parsed() { if *addBlockData == "" { addBlockCmd.Usage() os.Exit(1) } cli.addBlock(*addBlockData) } if printChainCmd.Parsed() { cli.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 } } }
這部分内容跟之前的很像,唯一的差別是我們現在使用的是
BlockchainIterator
對區塊鍊中的區塊進行疊代:
記得不要忘了對
main
函數作出相應的修改:
func main() { bc := NewBlockchain() defer bc.db.Close() cli := CLI{bc} cli.Run() }
注意,無論提供什麼指令行參數,都會建立一個新的鍊。
這就是今天的所有内容了! 來看一下是不是如期工作:
$ blockchain_go printchain No existing blockchain found. Creating a new one... Mining the block containing "Genesis Block" 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b Prev. hash: Data: Genesis Block Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true $ blockchain_go addblock -data "Send 1 BTC to Ivan" Mining the block containing "Send 1 BTC to Ivan" 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Success! $ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee" Mining the block containing "Pay 0.31337 BTC for a coffee" 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 Success! $ blockchain_go printchain Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Data: Pay 0.31337 BTC for a coffee Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 PoW: true Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b Data: Send 1 BTC to Ivan Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 PoW: true Prev. hash: Data: Genesis Block Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true
- 31
- 32
- 33
- 34
- 35
- 36
- 37

總結
在下篇文章中,我們将會實作位址,錢包,(可能實作)交易。盡請收聽!
原文釋出時間為:2017年10月04日
本文作者:liuchengxu_
本文來源:
CSDN,如需轉載請聯系原作者。