Go实现区块链(三)---存储与命令
1.前言
到目前为止我们了解区块链的数据结构以及简易版的挖矿(pow共识机制)。接下来我们将一起了解区块链的存储,注意:区块链本质上一款分布式数据库,这里不实现分布式,我们这先了解区块链存储部分。
2.知识准备
知识点 | 学习网页 | 特性 |
---|---|---|
比特币数据库 | leveldb | 1.key和value都是任意长度的字节数组;2.entry(即一条K-V记录)默认是按照key的字典顺序存储的,当然开发者也可以重载这个排序函数;3.提供的基本操作接口:Put()、Delete()、Get()、Batch();4.支持批量操作以原子操作进行;5.可以创建数据全景的snapshot(快照),并允许在快照中查找数据;6.可以通过前向(或后向)迭代器遍历数据(迭代器会隐含的创建一个snapshot);7.自动使用Snappy压缩数据;8、可移植性; |
BoltDB数据库 | boltDB | 1.它简单而简约;2.它在Go中实现;3.它不需要运行服务器;4.它允许构建我们想要的数据结构。 |
go中序列化 | 由于数据库是字节码的方式存储这里我们需要序列化对象,采用encoding/gob包 |
这里我们将会用道boltdb数据库来存储我们的数据。
3.数据结构
我们看看比特币数据库是怎么存储的。
简单理解,比特币使用了两个"buckets"(桶)来存储数据:
- blocks 描述链上所有区块的元数据。
- chainstate 存储区块链的状态,指的是当前所有的UTXO(未花费交易输出)以及一些元数据。
"在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO"
另外,块在磁盘上作为单独的文件存储。 这是为了达到性能目的而完成的:读取单个块不需要将全部(或部分)全部加载到内存中。 我们不会执行这个。
在blocks这个桶中,存储的是键值对:
#块索引记
'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
#各种可以打开或关闭的flag标志
'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 record
在 chainstate 这个桶中,存储的键值对:
#某笔交易的UTXO记录
'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
由于我们还没有交易,因此我们只会封装bucket。 另外,如上所述,我们将整个DB存储为单个文件,而不将块存储在单独的文件中。 所以我们不需要任何与文件编号有关的东西。 因此,这些是我们将使用的键 - >值对:
#区块数据与区块hash的键值对
32-byte block-hash -> Block structure (serialized)
#链中最后一个块的散列
'l' -> the hash of the last block in a chain
4.序列化
由于这里Key与Value采用[]byte的形式存储,所以我们需要序列化,采用Go提供的encoding/gob来实现序列化与反序列化。
//序列化Block
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
//反序列化
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
if err != nil {
log.Panic(err)
}
return &block
}
5.存储区块数据流程图
存储区块数据流程代码实现:
// 创建一个新的区块链和创世块
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 {
fmt.Println("No existing blockchain found. Creating a new one...")
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
if err != nil {
log.Panic(err)
}
bc := Blockchain{tip, db}
return &bc
}
这是打开BoltDB文件的标准方式。 注意,如果没有这样的文件,它不会返回错误。
添加区块方法AddBlock:现在我们添加区块并不会向数组添加元素那么简单,我们将block存储在DB中:
//添加区块
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte //最后一个区块hash
//查询数据库中最后一块的hash
err :=bc.db.View(func(tx *bolt.Tx) error {
b :=tx.Bucket([]byte(blocksBucket))
lastHash=b.Get([]byte("1"))//最新的一块hash的key我们知道为"l"
return nil
})
if err!=nil{
log.Panic(err)
}
//利最后的一块hash,挖掘一块新的区块出来
newBlock :=NewBlock(data,lastHash)
//在挖掘新块之后,我们将其序列化表示保存到数据块中并更新"l",该密钥现在存储新块的哈希。
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)
if err != nil {
log.Panic(err)
}
bc.tip = newBlock.Hash
return nil
})
}
好了存储区块桶实现了,接下来我们想实现查看区块链的区块数据。
6.检索区块链
BoltDB允许迭代桶中的所有键,但键以字节排序的顺序存储,我们希望块按照它们在区块链中的顺序进行打印。另外,因为我们不想将所有块加载到内存中(我们的区块链数据库可能很大,或者我们假装它可以),我们将逐个读取它们。为此,我们需要一个区块链迭代器:
// 区块链迭代器用于迭代区块
type BlockchainIterator struct {
currentHash []byte //当前的hash
db *bolt.DB //数据库
}
每次我们想要遍历区块链中的块时,都会创建一个迭代器,它将存储当前迭代的块散列和到数据库的连接。由于后者,迭代器在逻辑上被附加到区块链(它是一个Blockchain存储数据库连接的实例),因此在一个Blockchain方法中创建:
//迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
BlockchainIterator 只会做一件事:它会从区块链返回下一个区块。
// 迭代下一区块
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
}
7.CLI
目前为止我们并没有提供任何接口与程序交互,都是通过main函数里面来调用方法,我们想通过命令的方式来执行这些方法。封装一个cli:
type CLI struct {
bc *block.Blockchain //区块链
}
提供一个接口供main调用接口。
//启动接口函数
func Start(bc *block.Blockchain)interface{} {
cl := CLI{bc}
cl.run()//执行命令方法
return nil
}
//打印用法
func (cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data BLOCK_DATA - add a block to the blockchain")
fmt.Println(" printchain - print all the blocks of the blockchain")
}
//校验参数
func (cli *CLI) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
os.Exit(1)
}
}
//添加区块数据
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)
fmt.Printf("PoW: %s\n", strconv.FormatBool(block.Validate()))
fmt.Println()
//创世块是没有前一个区块的,所以PrevBlockHash的值是没有的
if len(block.PrevBlockHash) == 0 {
break
}
}
}
// 执行命令方法
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()
}
}
修改main
func main() {
bc := block.NewBlockchain()
defer block.Close(bc)
cli.Start(bc)
}
构建go项目命令:
C:\go-worke\src\github.com\study-bitcion-go>go build github.com/study-bitcion-go
命令方式添加一个数据:
C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go addblock -data "even send tom 1.000000BTC"
Mining the block containing "even send tom 1.000000BTC"
Dig into mine 0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635
Success!
迭代区块链数据:
C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go printchain
Prev. hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
Data: even send tom 1.000000BTC
Hash: 0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
PoW: true
本章实现了数据持久化存储,命令方式启动。后续我们将实现钱包、交易、网络等。
资料
- 原文来源:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
- 本文源码:https://github.com/Even521/study-bitcion-go/tree/part3
- java学习:https://www.jianshu.com/p/66c065018c7a
- 区块链基础视频学习:https://www.bilibili.com/video/av19620321/
- 区块链测试demo:https://anders.com/blockchain/blockchain.html
- 区块链QQ交流群:489512556