golang-区块链学习03永久存储

前言

前面两篇简单的实现了区块链的创建和工作量证明,但是都是在内存中进行的。实际的区块链应该是可以永久存储的,这样才有意义。下面开始做永久性区块链存储。

知识点

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、项目结构
golang-区块链学习03永久存储_第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[:]
}

你可能感兴趣的:(golang-区块链学习03永久存储)