用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 结论

下一节将构建带有地址、钱包和交易的区块链,不要走开哦!

猜你喜欢

转载自blog.csdn.net/liuzhijun301/article/details/82759760
今日推荐