在上一篇文章中我们构建了一个带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 结论
下一节将构建带有地址、钱包和交易的区块链,不要走开哦!