title: Golang实现区块链(三)—数据持久化(2)实现命令行查询
tags: go,blockchain,BoltDB
上篇文章我们使用BoltDb实现了对区块的读写,但是我们还是有很多的问题,例如我们每次运行,程序依然会从创世区块开始生成区块,还有我们缺少对生成的区块进行查询的功能。本文我们将完善这些问题。
之前我们对Blockchain_GenesisBlokc
只简单的实现了区块的写入,现在我们重新设计它的逻辑:
首先我们先定义一个dbExists
函数来判断,存储区块的桶是否存在,如果桶存在,说明我们之前生成过区块,并且写入了桶,所有判断桶是否存在,就是判断是否存在已经生成好的区块信息。
注意: 这里判断桶,我们排除手动创建的桶,即默认只有创世区块才能生成桶。
func dbExists() bool {
if _, err := os.Stat(dbName); os.IsNotExist(err) {
return false
}
return true
}
Blockchain_GenesisBlokc
接着我们对Blockchain_GenesisBlokc
进行完善
func Blockchain_GenesisBlokc() *Blockchain {
if dbExists(){
fmt.Println("区块已经存在。。。")
db, err := bolt.Open(dbName, 0600, nil)
if err != nil {
log.Panicf("open the Dbfailed! %v\n", err)
}
var blockchain *Blockchain
err =db.View(func(tx *bolt.Tx) error {
b:=tx.Bucket([]byte(bkName))
tip:=b.Get([]byte("l")) //这里使用的是 read-only事务的 Get 方法,从l中读取最后一块区块的编码,我们挖下一新块时会作为参数用到。
blockchain= &Blockchain{tip,db}
return nil
})
if err !=nil{
log.Panicf("get the block from db failed! %v\n", err)
}
return blockchain
}else{
db, err := bolt.Open(dbName, 0600, nil)
if err != nil {
log.Panicf("open the Dbfailed! %v\n", err)
}
//defer db.Close()
var tip []byte //存储数据库中的区块哈希
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bkName))
if b == nil {
b, err = tx.CreateBucket([]byte(bkName))
if err != nil {
log.Panicf("create the bucket [%s] failed! %v\n", bkName, err)
}
}
if b != nil {
genesisBlock := NewGenesisBlock()
//存储创世区块
err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())
if err != nil {
log.Panicf("put the data of genesisBlock to Dbfailed! %v\n", err)
}
//存储最新区块链哈希
err = b.Put([]byte("l"), genesisBlock.Hash)//在挖出新块,将其序列化存储到数据库后,把最新的区块hash值更新到 l 值中。
if err != nil {
log.Panicf("put the hash of latest block to Dbfailed! %v\n", err)
}
tip = genesisBlock.Hash
}
return nil
})
if err != nil {
log.Panicf("update the data of genesis block failed! %v\n", err)
}
return &Blockchain{tip, db}
}
}
现在所有产生的区块都会被保存到数据库里面,我们想要区块能够顺序打印,BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储。当数据很大的时候,我们并不想让所有的数据都直接加载到内存中,所有我们需要一个迭代器,来一个一个读取区块。
每当要对链中的块进行迭代时,就会创建一个迭代器,里面存储了当前迭代的块哈希(CurrentHash)和数据库的连接(DB)。
// 区块链迭代器结构
type BlockChainIterator struct {
DB *bolt.DB // 数据库
CurrentHash []byte // 当前区块的哈希值
}
在每次我们要去遍历整个区块链中的区块时会创建一个该遍历器。遍历器会保存当前遍历到的区块hash和保持与数据库的链接,后者也使得遍历器和该区块链在逻辑上是结合的,因为遍历器数据库连接用的是区块链的同一个,所以,Blockchain 会负责创建遍历器:
// 创建迭代器对象
func (blc *BlockChain) Iterator() *BlockChainIterator {
return &BlockChainIterator{blc.DB, blc.Tip}
}
因为我们是通过Tip来遍历的,所以迭代器的是从新到旧进行获取。
BlockchainIterator 只做一件事:它负责返回区块链中的下一个区块:
func (bcit *BlockChainIterator)Next() *Block {
var block *Block
err := bcit.DB.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockTableName))
if nil != b {
// 获取指定哈希的区块数据
currentBlockBytes := b.Get(bcit.CurrentHash)
block = DeserializeBlock(currentBlockBytes)
// 更新迭代器中当前区块的哈希值
bcit.CurrentHash = block.PrevBlockHash
}
return nil
})
if nil != err {
log.Panicf("iterator the db of blockchain failed! %v\n", err)
}
return block
}
到了这里区块链的持久化已经完成了,但是我们还没有一个交互接口,能让我们手动添加区块和打印区块链,现在我们就来实现它。
type CLI struct {
BC *BlockChain
}
我们先设计下调用命令:
createblockchain – 创建区块链.
addblock -data DATA – 交易数据
printchain – 输出区块链的信息
// 展示用法
func PrintUsage() {
fmt.Println("Usage:")
fmt.Printf("\tcreateblockchain -- 创建区块链.\n")
fmt.Printf("\taddblock -data DATA -- 交易数据\n")
fmt.Printf("\tprintchain -- 输出区块链的信息\n")
}
校验,如果只输入了程序命令,就输出指令用法并且退出程序
func IsValidArgs() {
if len(os.Args) < 2 {
PrintUsage() // 打印用法
os.Exit(1) // 退出程序
}
}
// 添加区块
func (cli *CLI) addBlock(data string) {
cli.BC.AddBlock([]byte(data))
}
// 输出区块链信息
func (cli *CLI) printchain() {
cli.BC.PrintChain()
}
// 创建区块链
func (cli *CLI) createBlockchainWithGenesis() {
CreateBlockChainWithGenesisBlock()
}
使用flag 获取命令行输出参数,然后通过switch来判断参数内容执行相应的函数。
func (cli *CLI) Run() {
// 1. 检测参数数量
IsValidArgs()
// 2. 新建命令
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
createBlCWithGenesisCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError)
// 3. 获取命令行参数
flagAddBlockArg:=addBlockCmd.String("data","send 100 BTC to everyone","交易数据")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
if nil != err {
log.Panicf("parse cmd of add block failed! %v\n", err)
}
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
if nil != err {
log.Panicf("parse cmd of printchain failed! %v\n", err)
}
case "createblockchain":
err := createBlCWithGenesisCmd.Parse(os.Args[2:])
if nil != err {
log.Panicf("parse cmd of create block chain failed! %v\n", err)
}
default:
PrintUsage()
os.Exit(1)
}
// 添加区块命令
if addBlockCmd.Parsed() {
if *flagAddBlockArg == "" {
PrintUsage()
os.Exit(1)
}
cli.addBlock(*flagAddBlockArg)
}
// 输出区块链信息命令
if printChainCmd.Parsed() {
cli.printchain()
}
// 创建区块链
if createBlCWithGenesisCmd.Parsed() {
cli.createBlockchainWithGenesis()
}
}
在运行前我们首先要修改下main函数
func main() {
blockChain := BLC.Blockchain_GenesisBlokc()
defer blockChain.Db.Close();
//blockChain.AddBlock("Send 100 btc to troy")
//blockChain.AddBlock("Send 50 btc to Alice")
//blockChain.AddBlock("Send 20 btc to Bob")
BLC.PrintUsage()
cli := BLC.CLI{blockChain}
cli.Run()
}
我们打开终端,进入项目所在文件夹,输入go build ./ 构建项目,得到执行文件。
不输入参数直接执行文件输出:
区块已经存在。。。
Usage:
createblockchain -- 创建区块链.
addblock -data DATA -- 交易数据
printchain -- 输出区块链的信息
因为我们前面已经往数据库中添加过区块,所有本文直接演示打印区块链。在终端输入 ./Blockchain03 - printblockchain (Blockchain03是本文项目的名称) 输出:
区块已经存在。。。
Usage:
createblcwithgenesis -- 创建区块链.
addblock -data DATA -- 交易数据
printblockchain -- 输出区块链的信息
——————————————打印区块链———————————————————————
----------------------------------------
Heigth : 3
TimeStamp : 1539140742
PrevBlockHash : 00000c20760b2347db31e5caffa6ca26869af13d2eba487e3be3dc593cad9265
Hash : 000002f3549162393dd88ccd0b550b039b6faa7af13f2db7a27442d36708253e
Data : Send 20 btc to Bob
Nonce : 569774
----------------------------------------
Heigth : 2
TimeStamp : 1539140728
PrevBlockHash : 000003b8099976553fe07fc59421072b19a99b0bbdc25032d3b161420a32c840
Hash : 00000c20760b2347db31e5caffa6ca26869af13d2eba487e3be3dc593cad9265
Data : Send 50 btc to Alice
Nonce : 2809923
----------------------------------------
Heigth : 1
TimeStamp : 1539140726
PrevBlockHash : 000006c70de89f4c92f0fe5c57f96a7b65882096d4fe50c6ad737f8feb485661
Hash : 000003b8099976553fe07fc59421072b19a99b0bbdc25032d3b161420a32c840
Data : Send 100 btc to troy
Nonce : 369529
----------------------------------------
Heigth : 0
TimeStamp : 1539089785
PrevBlockHash :
Hash : 000006c70de89f4c92f0fe5c57f96a7b65882096d4fe50c6ad737f8feb485661
Data : first block
Nonce : 523303
好了我们终于实现了区块的持久化,还有完善了遍历信息来支持按序打印所有的区块,已经交互命令,下一章我们将了解到交易的实现。