开发环境:Go语言
本教程是学习Jeiwan的博客后的学习笔记,代码实现也参考它的为主,精简了叙述并在适当位置添加了一些必备的小知识和适当的代码注释,如介绍哈希。
本教程是为了逐步教你设计一款简化的区块链最简比特币。通过我们不断添加功能,完成一个可交易的最简比特币。
本节我们增加持久化的功能,可以持久化区块链到本地文件。
- 单机版,仅支持保存信息✅
- 工作量证明✅
- 持久化
选择数据库
目前,我们的原型币是存储在内存里的,每次运行结束后消失。而真正的比特币账本是需要持久化保存到本地的,比特币的核心开发者们,选择的是 LevelDB,是一个键值存储的数据库,类似的,我们也选取一个键值对存储的数据库,他是轻量级的、用Go语言实现BoltDB
BoltDB
他的数据存取都是用的键,可以当作一个Map来使用。
需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。因此,对于一些Go语言的结构化数据如struct,我们使用标准库encoding/gob来做转换。
数据库的结构设计
比特币的实现使用了两个数据库(在键值对数据库中称为“bucket”)存储区块链。
- “block”,存储了描述一条链中所有块的元数据
- “chainstate”,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据。
值得注意的是,比特币为了节约内存,将每个区块(block)作为一个文件存储为磁盘上。
我们为了简单实现,会在一个文件包含全部区块链。
我们会用到的键值对有:
- 32 字节的 block-hash -> block 结构
- l -> 链中最后一个块的 hash
序列化和反序列化
将我们的Block结构体,序列化为[]byte,以及把[]byte反序列化为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
}
如果搞不清Go语言的读写数据的代码,可参考这篇文章Go语言标准库 的第一章
区块链的持久化
首先,改变我们只抢Blockchain的struct结构,
type Blockchain struct {
Tip []byte //表示最后一个区块的[]byte表示
DB *bolt.DB //数据库的指针
}
注意这里的改动,将原先的[]*Block去掉,因为原先相当于把全部Block读取出来放入了内存,如果数据量大则会爆内存。要节约内存,只能保存一个数据库的指针,每次根据需要去数据库里查出来对应的区块。这里用tip存储最新的区块的hash。
创建区块链的算法如下:
- 打开一个数据库文件
- 检查文件里面是否已经存储了一个区块链
- 如果已经存储了一个区块链:
- 创建一个新的
Blockchain
实例 - 设置
Blockchain
实例的 tip 为数据库中存储的最后一个块的哈希
- 创建一个新的
- 如果没有区块链:
- 创建创世块
- 存储到数据库
- 将创世块哈希保存为最后一个块的哈希
- 创建一个新的
Blockchain
实例,初始时 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)
算法实现的代码大概是这样:
func NewBlockchain() *Blockchain {
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
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
}) //注意看这里是丢了一个匿名函数进去,让BoltDB去执行update
bc := Blockchain{tip, db}
return &bc
}
接下来更新添加区块的方法,算法是:
- 查库取出最新区块的hash。
- 挖矿新区块。
- 再添加回数据库
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
})
newBlock := NewBlock(data, lastHash) //这里不查库,直接传bc.tip也可以,因为他只需最后一个区块的hash。既然你能取到blockchain的db了,相当于也就tip有值即最后一个区块的hash
err = bc.DB.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return nil
})
}
至此,我们的区块链持久化已经成功,但是目前有个缺点,就是不再能直接遍历区块链了。
遍历区块链
笨办法是全部读取到内存,但是这样就违背了我们设计Blockchain的结构的初衷。
因此我们要用迭代器来,逐个逐个的从数据库取数据。
type BlockchainIterator struct {
CurrentHash []byte
DB *bolt.DB
}
然后给Blockchain结构加上迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.Tip, bc.DB}
return bci
}
并实现Next接口
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
})
i.CurrentHash = block.PrevHash //为下一个查库作准备
return block
}
最后,让我们打印一下这个区块链。
func printChain(blockchain *Blockchain) {
bci := blockchain.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevHash)
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.PrevHash) == 0 {
break
}
}
}
调用main函数如下
func main() {
blockchain := NewBlockchain()
blockchain.AddBlock("Block1: 1 BTC to Lin")
blockchain.AddBlock("Block2: 2 BTC to Lin")
printChain(blockchain)
}
记得每次删除db重置数据库哦!
控制台
如果你希望便利的通过控制台直接指挥程序做事,可以通过Go语言的标准库flag实现。代码如下:
type CLI struct {
Blockchain *Blockchain
}
func (cli *CLI) run() {
if len(os.Args) < 2 {//控制台,当前运行的文件名是os.Args[0],还需要一个参数才可以执行指令
flag.Usage()
return
}
addBlockCmd := flag.NewFlagSet("addBlock", flag.ExitOnError)
addBlockMsg := addBlockCmd.String("data", "", "Your block message") //通过控制台输入“-data xxxx”,得到一个xxx字符串的addBlockMsg填入区块
printChainCmd := flag.NewFlagSet("printChain", flag.ExitOnError)
switch os.Args[1] {
case "addBlock":
addBlockCmd.Parse(os.Args[2:])//flag帮助解析,-data后的指令
case "printChain":
printChainCmd.Parse(os.Args[2:])
default:
flag.Usage()
return
}
if addBlockCmd.Parsed() { //如果用了addBlock指令,则只抢的Parse调用后,该指令的Paresed为真
if *addBlockMsg == "" {
addBlockCmd.Usage()
return
} //Usage会帮助打印使用说明
addBlock(cli.Blockchain, *addBlockMsg)
}
if printChainCmd.Parsed() {
printChain(cli.Blockchain)
}
}
func addBlock(blockchain *Blockchain, data string) {
blockchain.AddBlock(data)
}
参考:
Building Blockchain in Go. Part 3: Persistence and CLI,jiewan
源码
https://github.com/linxinzhe/go-simple-coin/tree/3_persistence
下一节:
全系列:
关于我:
linxinzhe,全栈工程师,目前供职于某500强通信企业,人工智能,区块链爱好者。
GitHub:https://github.com/linxinzhe
欢迎留言讨论,也欢迎关注我,收获更多区块链开发相关的知识,我也会关注你的哦!
网友评论