天天看點

用 Go 建構一個區塊鍊 -- Part 3: 持久化和指令行接口

引言

到目前為止,我們已經建構了一個有工作量證明機制的區塊鍊。有了工作量證明,挖礦也就有了着落。雖然目前的實作離一個有着完整功能的區塊鍊越來越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我們會将區塊鍊持久化到一個資料庫中,然後會提供一個簡單的指令行接口,用來完成一些與區塊鍊的互動操作。本質上,區塊鍊是一個分布式資料庫,不過,我們暫時先忽略 “分布式” 這個部分,僅專注于 “存儲” 這一點。

選擇資料庫

目前,我們的區塊鍊實作裡面并沒有用到資料庫,而是在每次運作程式時,簡單地将區塊鍊存儲在記憶體中。那麼一旦程式退出,所有的内容就都消失了。我們沒有辦法再次使用這條鍊,也沒有辦法與其他人共享,是以我們需要把它存儲到磁盤上。

那麼,我們要用哪個資料庫呢?實際上,任何一個資料庫都可以。在 比特币原始論文 中,并沒有提到要使用哪一個具體的資料庫,它完全取決于開發者如何選擇。 Bitcoin Core ,最初由中本聰釋出,現在是比特币的一個參考實作,它使用的是  LevelDB。而我們将要使用的是…

BoltDB

因為它:

  1. 非常簡單和簡約
  2. 用 Go 實作
  3. 不需要運作一個伺服器
  4. 能夠允許我們構造想要的資料結構

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” 來存儲資料:

  1. 其中一個 bucket 是 blocks,它存儲了描述一條鍊中所有塊的中繼資料
  2. 另一個 bucket 是 chainstate,存儲了一條鍊的狀态,也就是目前所有的未花費的交易輸出,和一些中繼資料

此外,出于性能的考慮,Bitcoin Core 将每個區塊(block)存儲為磁盤上的不同檔案。如此一來,就不需要僅僅為了讀取一個單一的塊而将所有(或者部分)的塊都加載到記憶體中。但是,為了簡單起見,我們并不會實作這一點。

在 blocks 中,key -> value 為:

key value

b

 + 32 位元組的 block hash
block index record

f

 + 4 位元組的 file number
file information record

l

the last block file number used

R

 + 1 位元組的 boolean
是否正在 reindex

F

 + 1 位元組的 flag name length + flag name string
1 byte boolean: various flags that can be on or off

t

 + 32 位元組的 transaction hash
transaction index record

在 chainstate,key -> value 為:

c

unspent transaction output record for that transaction

B

32 位元組的 block hash: the block hash up to which the database represents the unspent transaction outputs

詳情可見 這裡。

因為目前還沒有交易,是以我們隻需要 blocks bucket。另外,正如上面提到的,我們會将整個資料庫存儲為單個檔案,而不是将區塊存儲在不同的檔案中。是以,我們也不會需要檔案編号(file number)相關的東西。最終,我們會用到的鍵值對有:

  1. 32 位元組的 block-hash -> block 結構
  2. l

     -> 鍊中最後一個塊的 hash

這就是實作持久化機制所有需要了解的内容了。

序列化

上面提到,在 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

 執行個體,并向其中加入創世塊。而現在,我們希望它做的事情有:

  1. 打開一個資料庫檔案
  2. 檢查檔案裡面是否已經存儲了一個區塊鍊
  3. 如果已經存儲了一個區塊鍊: 
    1. 建立一個新的 

      Blockchain

       執行個體
    2. 設定 

      Blockchain

       執行個體的 tip 為資料庫中存儲的最後一個塊的哈希
  4. 如果沒有區塊鍊: 
    1. 建立創世塊
    2. 存儲到資料庫
    3. 将創世塊哈希儲存為最後一個塊的哈希
    4. Blockchain

       執行個體,其 tip 指向創世塊(tip 有尾部,尖端的意思,在這裡 tip 存儲的是最後一個塊的哈希)

代碼大概是這樣:

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
用 Go 建構一個區塊鍊 -- Part 3: 持久化和指令行接口

總結

在下篇文章中,我們将會實作位址,錢包,(可能實作)交易。盡請收聽!

原文釋出時間為:2017年10月04日

本文作者:liuchengxu_

本文來源:

CSDN

,如需轉載請聯系原作者。