Golang实现区块链(三)—数据持久化(1)使用BoltDB

数据持久化

在前面的文章中,我们实现了能poW挖矿的区块链。但是我们之前的区块信息都是保存在缓存中的,每次我们运行都需要从创世区块开始,这显然是个重要缺陷,本章就将对持久化进行实现。在比特币中使用的是LevelDB来进行数据持久化,比特币系统和LevelDB都是用C++实现的。我们的区块链是用Go来实现的,所以我们也找来Go语言编写的BoltDB。

添加BoltDB

BoltDB是基于key/value存储,即是没有像SQL关系性数据库(MySQL、PG)那样的的表,也没有行、列。而数据只存在于Key-value结构中(和Golang的maps很像)。Key-value存放在和SQL的表功能差不多的桶(buckets)中,所以要得到值,就得知道“桶”和“key”

BoltDB有如下特性:

  • 小而简约
  • 使用Go实现
  • 不需要单独部署
  • 支持我们的数据结构

对于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.把创世区块添加到链中

写入DB

在写入数据之前,我们得先搞清楚怎么存储,再比特币中,它用了两个表存储数据:

  • blocks 存储了该链中所有的区块的元数据
  • chainstate 存储链的状态,储存当前未完成的事务信息及其它一些元数据。

在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 对:

  • 32-byte 该块哈希 -> 序列化后区块信息
  • ‘l’ -> 链中最后一个区块的hash值
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?是的你想的没错,本文并没有判断数据库存在的情况,也缺少查询区块数据的功能。别担心,下章我们将继续讲解数据持久化,我会添加区块链的迭代和命令行交互接口,来进一步完善我们的功能。

你可能感兴趣的:(go,#,goland实现区块链)