前言
前面两篇简单的实现了区块链的创建和工作量证明,但是都是在内存中进行的。实际的区块链应该是可以永久存储的,这样才有意义。下面开始做永久性区块链存储。
知识点
1、github项目引用
2、github.com/boltdb/bolt项目的简单使用
3、命令行使用
4、go常用的数据转换
golang-区块链永久性存储
1、创建区块链
方法:func NewBlockChain() *BlockChain
// 创建区块链
// 返回一个区块链实例
func NewBlockChain() *BlockChain {
var tip []byte
// 打开存储区块链的文件blockchian.db、不存在则创建
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
// 可读写的方式访问区块链文件blockchian.db
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockBucket))
if b == nil {
fmt.Println("No existing blockchain. Creating a new one...")
// 创建创世纪块
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blockBucket))
if err != nil {
log.Panic(err)
}
// 键值对的方式存储区块在blockchian.db里
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
// 以一个特殊的key保存最新的区块的hash,便于整个链的检索
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
}
bolt是一种通过键值对的方式来存储数据的。具体的介绍和使用参考github.com/boltdb/bolt。程序启动时候,调用NewBlockChain函数,打开blockchian.db文件实例化一个区块链对象BlockChain。如果是第一次运行程序会创建blockchian.db文件,并生成一个创世纪区块,存储进blockchian.db文件中,然后返回一个区块链对象。
2、添加新区块到链上
方法:func (bc *BlockChain) AddBlock(data string)
// 添加新区块到链上
// 参数:data,区块要保存的数据
func (bc *BlockChain) AddBlock(data string) {
var lastHash []byte
// 只读的方式打开blockchian.db,获取最新区块的hash值
err := bc.Db.View(func(tx1 *bolt.Tx) error {
b := tx1.Bucket([]byte(blockBucket))
lastHash = b.Get([]byte("l"))
return nil
})
if err != nil {
log.Panic(err)
}
// 计算新的区块
newBlock := NewBlock(data, lastHash)
bc.tip = newBlock.Hash
// 读写的方式打开lockchian.db,写入新区块到blockchian.db中。
bc.Db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockBucket))
if b == nil {
log.Panic("bucket is nil !")
}
err := b.Put(newBlock.Hash, newBlock.Serialize())
if err != nil {
log.Panic(err)
}
//更新最新区块的hash
err = b.Put([]byte("l"), newBlock.Hash)
if err != nil {
log.Panic(err)
}
return nil
})
}
添加新区块到链上,首先要获取当前链上最新区块的hash,然后计算新的区块,计算出新的区块后存储新区块数据到blockchian.db中。
3、区块数据序列化转换
将区块block实例转换成byte数组方法:func (b *Block)Serialize()[]byte
// 序列化一个区块实例为byte数组
func (b *Block)Serialize()[]byte {
var result bytes.Buffer
// 以一个byte的buf实例化一个编码实例encoder
encoder:=gob.NewEncoder(&result)
err:=encoder.Encode(b)
if err!=nil {
log.Panic(err)
}
return result.Bytes()
}
将byte数组转换成block对象方法:func Deserialize(b []byte)*Block
// 反序列化byte数组,生成block实例。
func Deserialize(b []byte)*Block{
var block Block
decoder:=gob.NewDecoder(bytes.NewReader(b))
err:=decoder.Decode(&block)
if err!=nil{
log.Panic(err)
}
return &block
}
4、命令行flag使用
// 命令行执行程序
func (cli *CLI) Run() {
cli.validateArgs()
// 创建命令行对象
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChianCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
// 命令行对象添加参数,
addBlockData := addBlockCmd.String("data", "", "区块数据不能为空!")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "printchain":
err:=printChianCmd.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 printChianCmd.Parsed(){
cli.printChain()
//cli.printUsage()
}
}
5、github项目引用
下载github.com/boltdb/bolt项目到工程目录,如附件项目结构图所示。
注意设置项目路径为gopath路径。
附件
1、项目结构2、代码
main.go
package main
import (
"core"
)
func main() {
// 创建区块链
bc := core.NewBlockChain()
// 关闭本地库
defer bc.Db.Close()
// 实例命令行对象
cli := core.CLI{bc}
cli.Run()
}
block.go
package core
import (
"time"
"strconv"
"bytes"
"crypto/sha256"
"encoding/gob"
"log"
)
type Block struct {
TimeStamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
pow := NewProofOfWork(block)
block.Nonce, block.Hash = pow.Run()
return block
}
func (b *Block) SetHash() {
strTimeStamp := []byte(strconv.FormatInt(b.TimeStamp, 10))
headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, strTimeStamp}, []byte{})
hash := sha256.Sum256(headers)
b.Hash = hash[:]
}
func NewGenesisBlock() *Block {
return NewBlock("Genesis Block", []byte{})
}
// 序列化一个区块实例为byte数组
func (b *Block)Serialize()[]byte {
var result bytes.Buffer
// 以一个byte的buf实例化一个编码实例encoder
encoder:=gob.NewEncoder(&result)
err:=encoder.Encode(b)
if err!=nil {
log.Panic(err)
}
return result.Bytes()
}
// 反序列化byte数组,生成block实例。
func Deserialize(b []byte)*Block{
var block Block
decoder:=gob.NewDecoder(bytes.NewReader(b))
err:=decoder.Decode(&block)
if err!=nil{
log.Panic(err)
}
return &block
}
blockchain.go
package core
import (
"fmt"
"log"
"github.com/boltdb/bolt"
)
const dbFile = "blockchian.db"
const blockBucket = "blocks"
type BlockChain struct {
tip []byte
Db *bolt.DB
}
type BLockchainIterator struct {
currentHash []byte
Db *bolt.DB
}
// 添加新区块到链上
// 参数:data,区块要保存的数据
func (bc *BlockChain) AddBlock(data string) {
var lastHash []byte
// 只读的方式打开lockchian.db,获取最新区块的hash值
err := bc.Db.View(func(tx1 *bolt.Tx) error {
b := tx1.Bucket([]byte(blockBucket))
lastHash = b.Get([]byte("l"))
return nil
})
if err != nil {
log.Panic(err)
}
// 计算新的区块
newBlock := NewBlock(data, lastHash)
bc.tip = newBlock.Hash
// 读写的方式打开lockchian.db,写入新区块到lockchian.db中。
bc.Db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockBucket))
if b == nil {
log.Panic("bucket is nil !")
}
err := b.Put(newBlock.Hash, newBlock.Serialize())
if err != nil {
log.Panic(err)
}
//更新最新区块的hash
err = b.Put([]byte("l"), newBlock.Hash)
if err != nil {
log.Panic(err)
}
return nil
})
}
func (bc *BlockChain) Iterator() *BLockchainIterator {
var lastHash []byte
bc.Db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(blockBucket))
lastHash = b.Get([]byte("l"))
//c := b.Cursor()
//for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
// fmt.Printf("key=%s, value=%s\n", k, v)
//}
return nil
})
return &BLockchainIterator{lastHash, bc.Db}
}
func (bci *BLockchainIterator) Next() *Block {
var byteBlock []byte
bci.Db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockBucket))
byteBlock = b.Get(bci.currentHash)
return nil
})
block := Deserialize(byteBlock)
bci.currentHash = block.PrevBlockHash
return block
}
// 创建区块链
// 返回一个区块链实例
func NewBlockChain() *BlockChain {
var tip []byte
// 打开存储区块链的文件blockchian.db、不存在则创建
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
// 可读写的方式访问区块链文件blockchian.db
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blockBucket))
if b == nil {
fmt.Println("No existing blockchain. Creating a new one...")
// 创建创世纪块
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blockBucket))
if err != nil {
log.Panic(err)
}
// 键值对的方式存储区块在blockchian.db里
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
// 以一个特殊的key保存最新的区块的hash,便于整个链的检索
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
}
cli.go
package core
import (
"fmt"
"os"
"flag"
"log"
"strconv"
)
type CLI struct {
Bc *BlockChain
}
func (cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data(区块的数据) - 添加一个区块到区块链上面去。")
fmt.Println(" printchain - 打印区块链上所有的区块")
}
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("Prive hash :%x\n",block.PrevBlockHash)
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.PrevBlockHash)==0{
break
}
}
}
// 命令行执行程序
func (cli *CLI) Run() {
cli.validateArgs()
// 创建命令行对象
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChianCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
// 命令行对象添加参数,
addBlockData := addBlockCmd.String("data", "", "区块数据不能为空!")
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "printchain":
err:=printChianCmd.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 printChianCmd.Parsed(){
cli.printChain()
//cli.printUsage()
}
}
proofofwork.go
package core
import (
"math"
"math/big"
"fmt"
"crypto/sha256"
"bytes"
)
var (
maxNonce = math.MaxInt64
)
const targetBits = 12
type ProofOfWork struct {
block *Block
target *big.Int
}
func NewProofOfWork(b *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,
IntToHex(pow.block.TimeStamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
}, []byte{})
return data
}
func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash [32]byte
nonce := 0
fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
for nonce < maxNonce {
data := pow.prepareData(nonce)
hash = sha256.Sum256(data)
fmt.Printf("\r%x", hash)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
fmt.Printf("\n\n")
return nonce, hash[:]
}
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
}
utils.go
package core
import (
"bytes"
"encoding/binary"
"log"
"crypto/sha256"
)
func IntToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
log.Panic(err)
}
return buff.Bytes()
}
func DataToHash(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}