【区块链技术怎么玩】之五go.3数据库存储链和实现命令⾏交互
到为⽌我们已经实现了⼀条能够进⾏pow的区块链,距离实现⼀条正真意义上的区块链还有很长的路需要⾛。我们可能已经发现每当我们关闭程序后重新只能重新创建新的链,之前的数据都会不存在了。原因就是我们⽬前的链存储的数据都是在内⽐特币使⽤的是⼀款简约⽽不简单的数据库LevelDB,这次我们不采⽤此数据库,⽽我们选择使⽤既简单⼜简约的BlotDB数据库。关于这款数据库的详细信息⼤家可以在⽹上去详细的了解,这⾥就不做详细的说明。简单的提⼀点的就是这款数据库在进⾏数据库存储之前,我们需要了解我们存储到数据库的有哪些信息。我们需要⽤到两个bucket(桶)来装我们的链信息。第⼀个bucket是存放blocks:⼀条链中包含的所有块中的元数据信息,另⼀个bucket是存放chainstate:存储链的状态信最终我们会⽤到的键值对为:
32 字节的 block-hash -> block 结构
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))
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 {error parse new
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()
}
}
//加⼊区块函数调⽤
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[]这个切⽚⾥⾯只包含这⼀个默认的参数,此时len(os.Args)=1,但是下⾯我们⽤到了os.Args[2:],所以已经超出范围了,所以要报错。后⾯下⾯是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)
return &block
}
2、blockchain包:
package blockchain
import (
"github/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
}
//迭代器的任务就是返回链中的下⼀个区块
//只读⽅式打开区块链数据库
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"
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论