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、项目结构
项目目录结构

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[:]
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容