1. 程式人生 > >用go編寫區塊鏈系列之3--持久化與命令列

用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對定義為:

  1. 'b' + 32-byte block hash -> block index record
  2. 'f' + 4-byte file number -> file information record
  3. 'l' -> 4-byte file number: the last block file number used
  4. 'R' -> 1-byte boolean: whether we're in the process of reindexing
  5. 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
  6. 't' + 32-byte transaction hash -> transaction index recor

在chainstate中,key/value對定義為:

  1. 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
  2. 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

由於我們還沒有引入交易,所以我們只需要有blocks桶。此外我們將所有資料儲存在一個檔案中,所以我們現在只需要定義倆個鍵值對:

  1. 32-byte block-hash -> Block structure (serialized)
  2. '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 結論

下一節將構建帶有地址、錢包和交易的區塊鏈,不要走開哦!