在前面的文章中,我们实现了能poW挖矿的区块链。但是我们之前的区块信息都是保存在缓存中的,每次我们运行都需要从创世区块开始,这显然是个重要缺陷,本章就将对持久化进行实现。在比特币中使用的是LevelDB来进行数据持久化,比特币系统和LevelDB都是用C++实现的。我们的区块链是用Go来实现的,所以我们也找来Go语言编写的BoltDB。
BoltDB是基于key/value存储,即是没有像SQL关系性数据库(MySQL、PG)那样的的表,也没有行、列。而数据只存在于Key-value结构中(和Golang的maps很像)。Key-value存放在和SQL的表功能差不多的桶(buckets)中,所以要得到值,就得知道“桶”和“key”
BoltDB有如下特性:
对于BoltDB的介绍和使用,可以参考我之前的文章,本文就不在探讨。
https://blog.csdn.net/yang731227/article/details/82974575
const dbName = "blockchain.db" //数据库
const bkName = "blocks" //桶
type Blockchain struct {
tip [] byte //存储最新区块的哈希值
Db *bolt.DB //数据库
}
我们定义了两个常量,dbName用来存储数据库名称,bkName用来存储桶/表的名称,桶用来存储区块信息。
Blockchain结构体我们添加了 tip用来存储最新区块的哈希值。
使用BlotDB的前提就是,它的K-V都只能存储byte数组,所以首先我们应该先把Block
转换成[]byte
,即数据序列化,反之当我们需要读取到区块数据的时候应该把[]byte
还原成Block
,即数据反序列化。
实现数据序列化
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder:=gob.NewEncoder(&result)
err :=encoder.Encode(b)
if err!=nil{
log.Panicf("serialize the block to byte failed %v \n",err)
}
return result.Bytes()
}
再实现解序列化方法
func DeserilizeBlock (blockBytes []byte) *Block{
var block Block
decoder:= gob.NewDecoder(bytes.NewReader(blockBytes))
err:= decoder.Decode(&block)
if err !=nil{
log.Panicf("deserialize the block to byte failed %v \n",err)
}
return &block
}
我们知道区块链是由创世区块引导的,所以我们首先应该对创世区块进行改造。前面的文章中我们使用NewBlockchain()
它是把创世区块添加到链中,显然现在这个方法不满足我们的需要,我们需要用其他方法进行替换,这里我们重新创建函数Blockchain_GenesisBlokc()
,这个函数需要实现:
1.创建DB文件,并打开
2.创建存储区块信息的桶
3.把创世区块的信息进行序列化,并存储DB中
4.把创世区块添加到链中
在写入数据之前,我们得先搞清楚怎么存储,再比特币中,它用了两个表存储数据:
在blocks 中,k-> v 对有:
‘b’+ 32-byte 该块的hash码 -> 块索引记录
‘f’ + 4-byte 文件编号 -> 文件信息记录
‘l’ -> 4-byte 文件编号: 最后一块文件的编号
‘R’ -> 1-byte 布尔值: 标记是否正在重置索引
‘F’ + 1-byte 标记名长度 + 标记名 -> 1 byte boolean: 各种可关可开的标记
‘t’ + 32-byte 交易的hash值 -> 交易的索引记录
因为我们现在还没有交易,所以现在只探讨blocks, chainstate我们展示不探讨。还有就是现在我们不把区块各自存在独立的文件中,而把整个DB当作一个文件存储Blocks。所以我们不需要任何关联到文件的数字。
所以我们现在只需要以下k->v 对:
func Blockchain_GenesisBlokc() *Blockchain {
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)
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}
}
改完链中创世区块后,然后我接着对函数AddBlock
进行改造。之前我们只是简单的把新的区块添加到链中,现在它需要做到:
1.取出桶中最后一个区块的Hash
2.对Hash进行反序列化,得到最后一个区块信息
3.根据取出的信息,创建新的区块
4.把新的区块序列化存储到DB中
func (bc *Blockchain) AddBlock(data string) {
err := bc.Db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bkName))
if b != nil {
blockBytes := b.Get(bc.tip)
latest_block := DeserilizeBlock(blockBytes)
newBlock := NewBlock(latest_block.Index+1, data, latest_block.Hash)
err := b.Put(newBlock.Hash, newBlock.Serialize())
if nil != err {
log.Panicf("put the data of new block into Dbfailed! %v\n", err)
}
err = b.Put([]byte("l"), newBlock.Hash)
if nil != err {
log.Panicf("put the hash of the newest block into Dbfailed! %v\n", err)
}
bc.tip = newBlock.Hash
}
return nil
})
if nil != err {
log.Panicf("update the Dbof block failed! %v\n", err)
}
}
写入DB都改造好了,现在我们来实现读取
func (bc *Blockchain) PrintChain() {
fmt.Println("——————————————打印区块链———————————————————————")
var curBlock *Block
var curHash []byte = bc.tip
for {
fmt.Println("—————————————————————————————————————————————")
bc.Db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bkName))
if b != nil {
blockBytes := b.Get(curHash)
curBlock = DeserilizeBlock(blockBytes)
fmt.Printf("\tHeigth : %d\n", curBlock.Index)
fmt.Printf("\tTimeStamp : %d\n", curBlock.TimeStamp)
fmt.Printf("\tPrevBlockHash : %x\n", curBlock.PrevBlockHash)
fmt.Printf("\tHash : %x\n", curBlock.Hash)
fmt.Printf("\tData : %s\n", string(curBlock.Data))
fmt.Printf("\tNonce : %d\n", curBlock.Nonce)
}
return nil
})
// 判断是否已经遍历到创世区块
var hashInt big.Int
hashInt.SetBytes(curBlock.PrevBlockHash)
if big.NewInt(0).Cmp(&hashInt) == 0 {
break // 跳出循环
}
curHash = curBlock.PrevBlockHash
}
}
func main() {
blockChain := BLC.Blockchain_GenesisBlokc()
defer blockChain.Db.Close();
blockChain.AddBlock("Send 100 btc to Jay")
blockChain.AddBlock("Send 50 btc to Clown")
blockChain.AddBlock("Send 20 btc to Bob")
blockChain.PrintChain()
}
本文实现了使用BoltDB,对数据进行持久化,但是运行的时候每次还是都是从创世区块开始,这是不是BUG?是的你想的没错,本文并没有判断数据库存在的情况,也缺少查询区块数据的功能。别担心,下章我们将继续讲解数据持久化,我会添加区块链的迭代和命令行交互接口,来进一步完善我们的功能。