到为止我们已经实现了一条能够进行pow的区块链,距离实现一条正真意义上的区块链还有很长的路需要走。我们可能已经发现每当我们关闭程序后重新只能重新创建新的链,之前的数据都会不存在了。原因就是我们目前的链存储的数据都是在内存中,并没有被写入电脑的磁盘,所以会随着程序的退出被擦除。这一节我们就要解决这个问题,使用数据库来永久地保存我们创建的链。
比特币使用的是一款简约而不简单的数据库LevelDB,这次我们不采用此数据库,而我们选择使用既简单又简约的BlotDB数据库。关于这款数据库的详细信息大家可以在网上去详细的了解,这里就不做详细的说明。简单的提一点的就是这款数据库中存储数据的方式是类似golang语言中的map结构,是使用键值对映射的方式来存储数据的,这些键值对被存储在一个叫bucket(桶)中。我们实例一个数据库就是实例一种bucket。比如如果需要查找一个值,我们首先要进入到装此值的桶是哪个,然后通过此值对应的key来查找。
在进行数据库存储之前,我们需要了解我们存储到数据库的有哪些信息。我们需要用到两个bucket(桶)来装我们的链信息。第一个bucket是存放blocks:一条链中包含的所有块中的元数据信息,另一个bucket是存放chainstate:存储链的状态信息。目前我们还没有涉及到交易,所以我们现在只考虑blocks bucket。
最终我们会用到的键值对为:
l
-> 链中最后一个块的 hash因为在数据库中存储的数据的变成[]byte,所以我们需要序列化和反序列化的操作,我们在block包里面实现这两个函数;
//0.3 实现Block的序列化
func (b *Block) Serialize() []byte {
//首先定义一个buffer存储序列化后的数据
var result bytes.Buffer
//实例化一个序列化实例,结果保存到result中
encoder := gob.NewEncoder(&result)
//对区块进行实例化
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
//0.3 实现反序列化函数
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
}
下面就要实现数据库的内容了:
让我们从 NewBlockchain
函数开始。在之前的实现中,它会创建一个新的Blockchain
实例,并向其中加入创世块。而现在,我们希望它做的事情有:
Blockchain
实例Blockchain
实例的 tip 为数据库中存储的最后一个块的哈希Blockchain
实例,其 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)代码大概是这样:
//实例化一个区块链,默认存储了创世区块
func NewBlockchain() *Blockchain {
//return &Blockchain{[]*block.Block{GenesisBlock()}}
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))
//查看名字为blocksBucket的Bucket是否存在
if b == nil {
//不存在则从头 创建
genesis := GenesisBlock() //创建创世区块
b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为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) //"l"键对应区块链顶端区块的哈希
if err != nil {
log.Panic(err)
}
tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
} else {
//如果存在blocksBucket桶,也就是存在区块链
//通过键"l"映射出顶端区块的Hash值
tip = b.Get([]byte("l"))
}
return nil
})
bc := Blockchain{tip,db} //此时Blockchain结构体字段已经变成这样了
return &bc
}
接下来我们想要更新的是 AddBlock
方法:现在向链中加入区块,就不是像之前向一个数组中加入一个元素那么简单了。从现在开始,我们会将区块存储在数据库里面:
//把区块添加进区块链
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")) //通过键"l"拿到区块链顶端区块哈希
return nil
})
if err != nil {
log.Panic(err)
}
//prevBlock := bc.Blocks[len(bc.Blocks)-1]
//求出新区块
newBlock := pow.NewBlock(data,lastHash)
// bc.Blocks = append(bc.Blocks,newBlock)
//把新区块加入到数据库区块链中
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)
bc.tip = newBlock.Hash
return nil
})
}
现在我们产生的区块都会保存在区块链数据库中,我们可以随时打开一条区块链加入新的区块。但是现在我们不能像以前一样通过for-range方式遍历区块链了。所以要打印出区块需要一个迭代器来帮助我们。通过区块链迭代器,我们能以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。
//分割线——————迭代器——————
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
//当需要遍历当前区块链时,创建一个此区块链的迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip,bc.db}
return bci
}
//迭代器的任务就是返回链中的下一个区块
func (i *BlockchainIterator) Next() *block.Block {
var Block *block.Block
//只读方式打开区块链数据库
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
//获取数据库中当前区块哈希对应的被序列化后的区块
encodeBlock := b.Get(i.currentHash)
//反序列化,获得区块
Block = block.DeserializeBlock(encodeBlock)
return nil
})
if err != nil {
log.Panic(err)
}
//把迭代器中的当前区块哈希设置为上一区块的哈希,实现迭代的作用
i.currentHash =Block.PrevBlockHash
return Block
}
为了方便我们在命令行界面操作我们的区块链,下面我们来实现CLI交互接口。首先创建一个CLI包,代码如下:
package CLI
import (
"fmt"
"os"
"flag"
"go_code/A_golang_blockchain/blockchain"
"go_code/A_golang_blockchain/pow"
"strconv"
"log"
)
//首先我们想要拥有这些命令 1.加入区块命令 2.打印区块链命令
//创建一个CLI结构体
type CLI struct {
BC *blockchain.Blockchain
}
//入口函数
func (cli *CLI) Run() {
//判断命令行输入参数的个数,如果没有输入任何参数则打印提示输入参数信息
cli.validateArgs()
//实例化flag集合
addBlockCmd := flag.NewFlagSet("addblock",flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain",flag.ExitOnError)
//注册一个flag标志符
addBlockData := addBlockCmd.String("data"," ","区块数据")
switch os.Args[1] { //os.Args为一个保存输入命令的切片
case "addblock":
//解析出"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() //如果没有输入标志位data,则提醒用户怎么正确的输入
os.Exit(1)
}
//用户输入正确则进行加入区块的操作
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
//打印区块链操作
cli.printChain()
}
}
//加入输入格式错误信息提示
func(cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data 区块信息")
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("成功加入区块...")
}
//打印区块链函数调用
func (cli *CLI) printChain() {
//这里需要用到迭代区块链的思想
//创建一个迭代器
bci := cli.BC.Iterator()
for {
block := bci.Next() //从顶端区块向前面的区块迭代
fmt.Printf("PrevHash:%x\n",block.PrevBlockHash)
fmt.Printf("Data:%s\n",block.Data)
fmt.Printf("Hash:%x\n",block.Hash)
//验证当前区块的pow
pow := pow.NewProofOfWork(block)
boolen := pow.Validate()
fmt.Printf("POW is %s\n",strconv.FormatBool(boolen))
fmt.Println()
if len(block.PrevBlockHash) == 0 {
break
}
}
}
注意:我之前没有写cli.validateArgs()这个函数来判断命令行输入参数的个数,然后就报错如下:
报错提示:panic: runtime error: index out of range,原因是如果我们没有输入任何参数的话,os.Args[]这个切片里面只包含main.go这一个默认的参数,此时len(os.Args)=1,但是下面我们用到了os.Args[2:],所以已经超出范围了,所以要报错。后面加上cli.validateArgs()函数来判断,就会有个提示我们要输入参数。
下面是main函数:
package main
import (
"go_code/A_golang_blockchain/blockchain"
"go_code/A_golang_blockchain/CLI"
)
func main() {
//先创建一条区块链
bc := blockchain.NewBlockchain()
//这里bc中的字段db由于是小写字母开头,所以我工厂模式了db,由函数Db()返回db
//程序退出前关闭数据库
defer bc.Db().Close()
cli := CLI.CLI{bc}
cli.Run()
// //加入区块到区块链中
// bc.AddBlock("区块01")
// bc.AddBlock("区块02")
//打印出区块链中各个区块的信息,并验证各个区块是否合格
// for _,b := range bc.Blocks {
// fmt.Printf("时间戳:%v\n",b.Timestamp)
// fmt.Printf("Data:%s\n",b.Data)
// fmt.Printf("上一区块哈希:%x\n",b.PrevBlockHash)
// fmt.Printf("Hash:%x\n",b.Hash)
// fmt.Printf("Nonce:%v\n",b.Nonce)
// //验证当前区块的pow
// pow := pow.NewProofOfWork(b)
// boolen := pow.Validate()
// fmt.Printf("POW is %s\n",strconv.FormatBool(boolen))
// fmt.Println()
// }
}
在main函数里面因为涉及到权限问题,结构体blockchain的db字段不能在外包访问,所以用工厂模式来调用字段db。
下面是这一章节完成后的整个代码:
1、block包:
package block
import (
"encoding/gob"
"bytes"
"log"
)
//区块的结构体
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
//0.3 实现Block的序列化
func (b *Block) Serialize() []byte {
//首先定义一个buffer存储序列化后的数据
var result bytes.Buffer
//实例化一个序列化实例,结果保存到result中
encoder := gob.NewEncoder(&result)
//对区块进行实例化
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
//0.3 实现反序列化函数
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
}
2、blockchain包:
package blockchain
import (
"github.com/boltdb/bolt"
"go_code/A_golang_blockchain/block"
"go_code/A_golang_blockchain/pow"
"log"
)
/*
区块链实现
*/
const dbFile = "blockchain.db"
const blocksBucket = "blocks"
const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
//区块链
type Blockchain struct {
tip []byte
db *bolt.DB
}
//工厂模式db
func(bc *Blockchain) Db() *bolt.DB {
return bc.db
}
//把区块添加进区块链
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")) //通过键"l"拿到区块链顶端区块哈希
return nil
})
if err != nil {
log.Panic(err)
}
//prevBlock := bc.Blocks[len(bc.Blocks)-1]
//求出新区块
newBlock := pow.NewBlock(data,lastHash)
// bc.Blocks = append(bc.Blocks,newBlock)
//把新区块加入到数据库区块链中
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)
bc.tip = newBlock.Hash
return nil
})
}
//创建创世区块
func GenesisBlock() *block.Block {
return pow.NewBlock("创世区块",[]byte{})
}
//实例化一个区块链,默认存储了创世区块
func NewBlockchain() *Blockchain {
//return &Blockchain{[]*block.Block{GenesisBlock()}}
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))
//查看名字为blocksBucket的Bucket是否存在
if b == nil {
//不存在则从头 创建
genesis := GenesisBlock() //创建创世区块
b,err := tx.CreateBucket([]byte(blocksBucket)) //创建名为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) //"l"键对应区块链顶端区块的哈希
if err != nil {
log.Panic(err)
}
tip = genesis.Hash //指向最后一个区块,这里也就是创世区块
} else {
//如果存在blocksBucket桶,也就是存在区块链
//通过键"l"映射出顶端区块的Hash值
tip = b.Get([]byte("l"))
}
return nil
})
bc := Blockchain{tip,db} //此时Blockchain结构体字段已经变成这样了
return &bc
}
//分割线——————迭代器——————
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
//当需要遍历当前区块链时,创建一个此区块链的迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip,bc.db}
return bci
}
//迭代器的任务就是返回链中的下一个区块
func (i *BlockchainIterator) Next() *block.Block {
var Block *block.Block
//只读方式打开区块链数据库
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
//获取数据库中当前区块哈希对应的被序列化后的区块
encodeBlock := b.Get(i.currentHash)
//反序列化,获得区块
Block = block.DeserializeBlock(encodeBlock)
return nil
})
if err != nil {
log.Panic(err)
}
//把迭代器中的当前区块哈希设置为上一区块的哈希,实现迭代的作用
i.currentHash =Block.PrevBlockHash
return Block
}
3、pow包:
package pow
import (
"fmt"
"crypto/sha256"
"strconv"
"bytes"
"math/big"
"go_code/A_golang_blockchain/block"
"math"
"time"
)
//在实际的比特币区块链中,加入一个区块是非常困难的事情,其中运用得到的就是工作量证明
//创建一个工作量证明的结构体
type ProofOfWork struct {
block *block.Block //要证明的区块
target *big.Int //难度值
}
//声明一个挖矿难度
const targetBits = 10
//实例化一个工作量证明
func NewProofOfWork(b *block.Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target,uint(256 - targetBits))
pow := &ProofOfWork{b,target}
return pow
}
//准备需要进行哈希的数据
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
[]byte(strconv.FormatInt(pow.block.Timestamp,10)),
[]byte(strconv.FormatInt(targetBits,10)),
[]byte(strconv.FormatInt(int64(nonce),10)),
},
[]byte{},
)
return data
}
//进行工作量证明,证明成功会返回随机数和区块哈希
func (pow *ProofOfWork) Run() (int,[]byte) {
nonce := 0
var hash [32]byte
var hashInt big.Int
for nonce < math.MaxInt64 {
data := pow.prepareData(nonce)
hash = sha256.Sum256(data)
hashInt.SetBytes(hash[:])
//把哈希后的数据与难度值进行比较
if hashInt.Cmp(pow.target) == -1 {
fmt.Printf("工作量证明成功 hash= %x nonce = %v\n",hash,nonce)
break
}else{
nonce ++
}
}
fmt.Println()
return nonce,hash[:]
}
//实例化一个区块
func NewBlock(data string,prevBlockHash []byte) *block.Block {
block := &block.Block{time.Now().Unix(),[]byte(data),prevBlockHash,[]byte{},0}
// block.SetHash()
pow := NewProofOfWork(block)
nonce,hash := pow.Run()
block.Hash = hash
block.Nonce = nonce
return block
}
//其他节点验证nonce是否正确
func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int
data := pow.prepareData(pow.block.Nonce)
hash := sha256.Sum256(data)
hashInt.SetBytes(hash[:])
isValid := hashInt.Cmp(pow.target) == -1
return isValid
}
4、CLI包:
package CLI
import (
"fmt"
"os"
"flag"
"go_code/A_golang_blockchain/blockchain"
"go_code/A_golang_blockchain/pow"
"strconv"
"log"
)
//首先我们想要拥有这些命令 1.加入区块命令 2.打印区块链命令
//创建一个CLI结构体
type CLI struct {
BC *blockchain.Blockchain
}
//入口函数
func (cli *CLI) Run() {
//判断命令行输入参数的个数,如果没有输入任何参数则打印提示输入参数信息
cli.validateArgs()
//实例化flag集合
addBlockCmd := flag.NewFlagSet("addblock",flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain",flag.ExitOnError)
//注册一个flag标志符
addBlockData := addBlockCmd.String("data"," ","区块数据")
switch os.Args[1] { //os.Args为一个保存输入命令的切片
case "addblock":
//解析出"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() //如果没有输入标志位data,则提醒用户怎么正确的输入
os.Exit(1)
}
//用户输入正确则进行加入区块的操作
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
//打印区块链操作
cli.printChain()
}
}
//加入输入格式错误信息提示
func(cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data 区块信息")
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("成功加入区块...")
}
//打印区块链函数调用
func (cli *CLI) printChain() {
//这里需要用到迭代区块链的思想
//创建一个迭代器
bci := cli.BC.Iterator()
for {
block := bci.Next() //从顶端区块向前面的区块迭代
fmt.Printf("PrevHash:%x\n",block.PrevBlockHash)
fmt.Printf("Data:%s\n",block.Data)
fmt.Printf("Hash:%x\n",block.Hash)
//验证当前区块的pow
pow := pow.NewProofOfWork(block)
boolen := pow.Validate()
fmt.Printf("POW is %s\n",strconv.FormatBool(boolen))
fmt.Println()
if len(block.PrevBlockHash) == 0 {
break
}
}
}
5、main包:
package main
import (
"go_code/A_golang_blockchain/blockchain"
"go_code/A_golang_blockchain/CLI"
)
func main() {
//先创建一条区块链
bc := blockchain.NewBlockchain()
//这里bc中的字段db由于是小写字母开头,所以我工厂模式了db,由函数Db()返回db
//程序退出前关闭数据库
defer bc.Db().Close()
cli := CLI.CLI{bc}
cli.Run()
// //加入区块到区块链中
// bc.AddBlock("区块01")
// bc.AddBlock("区块02")
//打印出区块链中各个区块的信息,并验证各个区块是否合格
// for _,b := range bc.Blocks {
// fmt.Printf("时间戳:%v\n",b.Timestamp)
// fmt.Printf("Data:%s\n",b.Data)
// fmt.Printf("上一区块哈希:%x\n",b.PrevBlockHash)
// fmt.Printf("Hash:%x\n",b.Hash)
// fmt.Printf("Nonce:%v\n",b.Nonce)
// //验证当前区块的pow
// pow := pow.NewProofOfWork(b)
// boolen := pow.Validate()
// fmt.Printf("POW is %s\n",strconv.FormatBool(boolen))
// fmt.Println()
// }
}
注释:上面的代码中注解都是很详细的,运行都能通过,在VScode编辑器中编辑时其中有两处有警告的提示,主要原因是我编写的Block结构体字段和CLI结构体字段的首字母都是大写的,警告提示为:Disable go vet checks for “composite literal uses unkeyed fields”,解释为我所使用的这两个结构体中的都是无键字段,所以警告,这种警告可以忽略的,是go vet的一个提示,并不是错误。
—— 如果以上有什么错误或者疑问欢迎指正,可以加我的联系方式进行讨论:微信(18382255942)——