为了账号安全,请及时绑定邮箱和手机立即绑定

从0到1简易区块链开发手册V0.3-数据持久化与创世区块

标签:
Go


Author: brucefeng

Email: brucefeng@brucefeng.com

编程语言:Golang

从0到1简易区块链开发手册V0.3-数据持久化与创世区块

1.BoltDB简介

Bolt是一个纯粹Key/Value模型的程序。该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供一个简单,快速,可靠的数据库。

BoltDB只需要将其链接到你的应用程序代码中即可使用BoltDB提供的API来高效的存取数据。而且BoltDB支持完全可序列化的ACID事务,让应用程序可以更简单的处理复杂操作。

其源码地址为:https://github.com/boltdb/bolt

2.BoltDB特性

BoltDB设计源于LMDB,具有以下特点:

使用Go语言编写

不需要服务器即可运行

支持数据结构

直接使用API存取数据,没有查询语句;

支持完全可序列化的ACID事务,这个特性比LevelDB强;

数据保存在内存映射的文件里。没有wal、线程压缩和垃圾回收;

通过COW技术,可实现无锁的读写并发,但是无法实现无锁的写写并发,这就注定了读性能超高,但写性能一般,适合与读多写少的场景。

BoltDB是一个Key/Value(键/值)存储,这意味着没有像SQL RDBMS(MySQL,PostgreSQL等)中的表,没有行,没有列。相反,数据作为键值对存储(如在Golang Maps中)。键值对存储在Buckets中,它们旨在对相似的对进行分组(这与RDBMS中的表类似)。因此,为了获得Value(值),需要知道该Value所在的桶和钥匙。

3.BoltDB简单使用

//通过go get下载并import 

import "github.com/boltdb/bolt" 

3.1 打开或创建数据库

db, err := bolt.Open("my.db", 0600, nil)

if err != nil {

   log.Fatal(err)

}

defer db.Close()

执行注意点

如果通过goland程序运行创建的my.db会保存在

GOPATH /src/Project目录下

如果通过go build main.go ; ./main 执行生成的my.db,会保存在当前目录GOPATH /src/Project/package下

3.2 数据库操作

(1) 创建数据库表与数据写入操作

//1. 调用Update方法进行数据的写入

err = db.Update(func(tx *bolt.Tx) error {

//2.通过CreateBucket()方法创建BlockBucket(表),初次使用创建

   b, err := tx.CreateBucket([]byte("BlockBucket"))

   if err != nil {

      return fmt.Errorf("Create bucket :%s", err)

   }

//3.通过Put()方法往表里面存储一条数据(key,value),注意类型必须为[]byte

   if b != nil {

      err := b.Put([]byte("l"), []byte("Send $100 TO Bruce"))

      if err != nil {

         log.Panic("数据存储失败..")

      }

   }

   return nil

})

//数据Update失败,退出程序

if err != nil {

   log.Panic(err)

}

(2) 数据写入

//1.打开数据库

db, err := bolt.Open("my.db", 0600, nil)

if err != nil {

   log.Fatal(err)

}

defer db.Close()

err = db.Update(func(tx *bolt.Tx) error {

//2.通过Bucket()方法打开BlockBucket表

   b := tx.Bucket([]byte("BlockBucket"))

//3.通过Put()方法往表里面存储数据

   if b != nil {

      err := b.Put([]byte("l"), []byte("Send $200 TO Fengyingcong"))

      err =  b.Put([]byte("ll"), []byte("Send $100 TO Bruce"))

      if err != nil {

         log.Panic("数据存储失败..")

      }

   }

   return nil

})

//更新失败

if err != nil {

   log.Panic(err)

}

(3) 数据读取

//1.打开数据库

db, err := bolt.Open("my.db", 0600, nil)

if err != nil {

   log.Fatal(err)

}

defer db.Close()

//2.通过View方法获取数据

err = db.View(func(tx *bolt.Tx) error {

//3.打开BlockBucket表,获取表对象

   b := tx.Bucket([]byte("BlockBucket"))

//4.Get()方法通过key读取value

   if b != nil {

      data := b.Get([]byte("l"))

      fmt.Printf("%s\n", data)

      data = b.Get([]byte("ll"))

      fmt.Printf("%s\n", data)

   }

   return nil

})

if err != nil {

   log.Panic(err)

}

4.通过BoltDB存储区块

该代码包含对BoltDB的数据库创建,表创建,区块添加,区块查询操作

//1.创建一个区块对象block

block := BLC.NewBlock("Send $500 to Tom", 1, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})

//2. 打印区块对象相关信息

fmt.Printf("区块的Hash信息为:\t%x\n", block.Hash)

fmt.Printf("区块的数据信息为:\t%v\n", string(block.Data))

fmt.Printf("区块的随机数为:\t%d\n", block.Nonce)

//3. 打开数据库

db, err := bolt.Open("my.db", 0600, nil)

if err != nil {

   log.Fatal(err)

}

defer db.Close()

//4. 更新数据

err = db.Update(func(tx *bolt.Tx) error {

//4.1 打开BlockBucket表对象

   b := tx.Bucket([]byte("blocks"))

//4.2 如果表对象不存在,创建表对象

   if b == nil {

      b, err = tx.CreateBucket([]byte("blocks"))

      if err != nil {

         log.Panic("Block Table Create Failed")

      }

   }

   //4.3 往表里面存储一条数据(key,value)

   err = b.Put([]byte("l"), block.Serialize())

   if err != nil {

      log.Panic("数据存储失败..")

   }

   return nil

})

//更新失败,返回错误

if err != nil {

   log.Panic("数据更新失败")

}

//5. 查看数据

err = db.View(func(tx *bolt.Tx) error {

//5.1打开BlockBucket表对象

   b := tx.Bucket([]byte("blocks"))

   if b != nil {

       //5.2 取出key=“l”对应的value

      blockData := b.Get([]byte("l"))

        //5.3反序列化   

      block := BLC.DeserializeBlock(blockData)

 //6. 打印区块对象相关信息

    fmt.Printf("区块的Hash信息为:\t%x\n", block.Hash)

    fmt.Printf("区块的数据信息为:\t%v\n", string(block.Data))

    fmt.Printf("区块的随机数为:\t%d\n", block.Nonce)

   }

   return nil

})

//数据查看失败

if err != nil {

   log.Panic("数据更新失败")

}

五.创建创世区块

从0到1简易区块链开发手册V0.3-数据持久化与创世区块

1.概念

北京时间2009年1月4日2时15分5秒,比特币的第一个区块诞生了。随着时间往后推移,不断有新的区块被添加到链上,所有后续区块都可以追溯到第一个区块。第一个区块就被人们称为创世区块。

2. 工作量证明

在比特币世界中,获取区块记账权的过程称之为挖矿,一个矿工成功后,他会把之前打包好的网络上的交易记录到一页账本上,同步给其他人。因为这个矿工能够最先计算出超难数学题的正确答案,说明这个矿工付出了工作量,是一个有权利记账的人,因此其他人也会同意这一页账单。这种依靠工作量来证明记账权,大家来达成共识的机制叫做“工作量证明”,简而言之结果可以证明你付出了多少工作量。Proof Of Work简称“PoW”,关于其原理跟代码实现,我们在后面的代码分析中进行讲解说明。

2.1 定义结构体

type ProofOfWork struct {

    Block  *Block   //要验证的block

    Target *big.Int //目标hash

}

2.2 创建工作量证明对象

const TargetBit = 16 //目标哈希的0个个数,16,20,24,28

func NewProofOfWork(block *Block) *ProofOfWork {

    //1.创建pow对象

    pow := &ProofOfWork{}

    //2.设置属性值

    pow.Block = block

    target := big.NewInt(1)           // 目标hash,初始值为1

    target.Lsh(target, 256-TargetBit) //左移256-16

    pow.Target = target

    return pow

}

我们首先设定一个难度系数值为16,即目标哈希前导0的个数,0的个数越多,挖矿难度越大,此处我们创建一个函数NewProofOfWork用于返回Pow对象。

目标Hash的长度为256bit,通过64个16进制byte进行展示,如下所示为前导0为16/4=4的哈希

0000c01d342fc51cb030f93979343de70ab771855dd8ca28e6f5888737759747

通过big.NewInt创建一个BigInt对象target

对target进行通过左移(256-TargetBit)位操作

2.3 将int64类型转[]byte

func IntToHex(num int64) []byte {

    buff := new(bytes.Buffer)

    //将二进制数据写入w

    //

    err := binary.Write(buff, binary.BigEndian, num)

    if err != nil {

        log.Panic(err)

    }

    //转为[]byte并返回

    return buff.Bytes()

}

通过func Write(w io.Writer, order ByteOrder, data interface{}) error方法将一个int64的整数转为二进制后,每8bit一个byte,转为[]byte

2.4 拼接区块属性数据

func (pow *ProofOfWork) prepareData(nonce int64) []byte {

    data := bytes.Join([][]byte{

        IntToHex(pow.Block.Height),

        pow.Block.PrevBlockHash,

        IntToHex(pow.Block.TimeStamp),

        pow.Block.HashTransactions(),

        IntToHex(nonce),

        IntToHex(TargetBit),

    }, []byte{})

    return data

}

通过bytes.Join方法将区块相关属性进行拼接成字节数组

2.5 "挖矿"方法

func (pow *ProofOfWork) Run() ([]byte, int64) {

    var nonce int64 = 0

    var hash [32]byte

    for {

        //1.根据nonce获取数据

        data := pow.prepareData(nonce)

        //2.生成hash

        hash = sha256.Sum256(data) //[32]byte

        fmt.Printf("\r%d,%x", nonce, hash)

        //3.验证:和目标hash比较

        /*

        func (x *Int) Cmp(y *Int) (r int)

        Cmp compares x and y and returns:

           -1 if x <  y

            0 if x == y

           +1 if x >  y

        目的:target > hashInt,成功

         */

        hashInt := new(big.Int)

        hashInt.SetBytes(hash[:])

        if pow.Target.Cmp(hashInt) == 1 {

            break

        }

        nonce++

    }

    fmt.Println()

    return hash[:], nonce

}

代码思路

设置nonce值:0,1,2.......

block-->拼接数组,产生hash

比较实际hash和pow的目标hash

不断更改nonce的值,计算hash,直到小于目标hash。

2.6 验证区块

func (pow *ProofOfWork) IsValid() bool {

   hashInt := new(big.Int)

   hashInt.SetBytes(pow.Block.Hash)

   return pow.Target.Cmp(hashInt) == 1

}

判断方式同挖矿中的策略

3.区块创建

3.1 定义结构体

type Block struct {

    //字段属性

    //1.高度:区块在区块链中的编号,第一个区块也叫创世区块,一般设定为0

    Height int64

    //2.上一个区块的Hash值

    PrevBlockHash []byte

    //3.数据:Txs,交易数据

    Txs []*Transaction

    //4.时间戳

    TimeStamp int64

    //5.自己的hash

    Hash []byte

    //6.Nonce

    Nonce int64

}

关于属性的定义,在代码的注释中比较清晰了,需要提一下的就是创世区块的PrevBlockHash一般设定为0 ,高度也一般设定为0

3.2 创建创世区块

func CreateGenesisBlock(txs []*Transaction) *Block{

    return NewBlock(txs,make([]byte,32,32),0)

}

设定创世区块的PrevBlockHash为0,区块高度为0

3.3 序列化区块对象

func (block *Block) Serialize()[]byte{

    //1.创建一个buff

    var buf bytes.Buffer

    //2.创建一个编码器

    encoder:=gob.NewEncoder(&buf)

    //3.编码

    err:=encoder.Encode(block)

    if err != nil{

        log.Panic(err)

    }

    return buf.Bytes()

}

通过gob库的Encode方法将Block对象序列化成字节数组,用于持久化存储

3.4 字节数组反序列化

func DeserializeBlock(blockBytes [] byte) *Block{

    var block Block

    //1.先创建一个reader

    reader:=bytes.NewReader(blockBytes)

    //2.创建×××

    decoder:=gob.NewDecoder(reader)

    //3.解码

    err:=decoder.Decode(&block)

    if err != nil{

        log.Panic(err)

    }

    return &block

}

定义一个函数,用于将[]byte反序列化为block对象

4.区块链创建

4.1 定义结构体

type BlockChain struct {

    DB  *bolt.DB //对应的数据库对象

    Tip [] byte  //存储区块中最后一个块的hash值

}

定义区块链结构体属性DB用于存储对应的数据库对象,Tip用于存储区块中最后一个块的Hash值

4.2 判断数据库是否存在

const DBName  = "blockchain.db" //数据库的名字

const BlockBucketName = "blocks" //定义bucket

定义数据库名字以及定义用于存储区块数据的bucket(表)名

func dbExists() bool {

    if _, err := os.Stat(DBName); os.IsNotExist(err) {  

        return false //表示文件不存在

    }

    return true //表示文件存在

}

需要注意IsNotExist返回true,则表示不存在成立,返回值为true,则dbExists函数的返回值则需要返回false,否则,返回true

4.3 创建带有创世区块的区块链

func CreateBlockChainWithGenesisBlock(address string) {

    /*

    1.判断数据库如果存在,直接结束方法

    2.数据库不存在,创建创世区块,并存入到数据库中

     */

    if dbExists() {

        fmt.Println("数据库已经存在,无法创建创世区块")

        return

    }

    //数据库不存在

    fmt.Println("数据库不存在")

    fmt.Println("正在创建创世区块")

    /*

    1.创建创世区块

    2.存入到数据库中

     */

    //创建一个txs--->CoinBase

    txCoinBase := NewCoinBaseTransaction(address)

    genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})

    db, err := bolt.Open(DBName, 0600, nil)

    if err != nil {

        log.Panic(err)

    }

    defer db.Close()

    err = db.Update(func(tx *bolt.Tx) error {

        //创世区块序列化后,存入到数据库中

        b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))

        if err != nil {

            log.Panic(err)

        }

        if b != nil {

            err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())

            if err != nil {

                log.Panic(err)

            }

            b.Put([]byte("l"), genesisBlock.Hash)

        }

        return nil

    })

    if err != nil {

        log.Panic(err)

    }

    //return &BlockChain{db, genesisBlock.Hash}

}

代码分析

(1) 判断数据库是否存在,如果不存在,证明还没有创建创世区块,如果存在,则提示创世区块已存在,直接返回

    if dbExists() {

        fmt.Println("数据库已经存在,无法创建创世区块")

        return

    }

(2) 如果数据库不存在,则提示开始调用相关函数跟方法创建创世区块

    fmt.Println("数据库不存在")

    fmt.Println("正在创建创世区块")

(3) 创建一个交易数组Txs

关于交易这一部分内容,将在后面一个章节中进行详细说明,篇幅会非常长,这也是整个课程体系中最为繁琐,知识点最广的地方,届时慢慢分析

txCoinBase := NewCoinBaseTransaction(address)

通过函数NewCoinBaseTransaction创建一个CoinBase交易

func NewCoinBaseTransaction(address string) *Transaction {

   txInput := &TxInput{[]byte{}, -1, nil, nil}

   txOutput := NewTxOutput(10, address)

   txCoinBaseTransaction := &Transaction{[]byte{}, []*TxInput{txInput}, []*TxOutput{txOutput}}

   //设置交易ID

   txCoinBaseTransaction.SetID()

   return txCoinBaseTransaction

}

(4) 生成创世区块

genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})

(5) 打开/创建数据库

    db, err := bolt.Open(DBName, 0600, nil)

    if err != nil {

        log.Panic(err)

    }

    defer db.Close()

通过bolt.Open方法打开(如果不存在则创建)数据库文件,注意数据库关闭操作不能少,用defer实现延迟关闭。

(6) 将数据写入数据库

    err = db.Update(func(tx *bolt.Tx) error {

        b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))

        if err != nil {

            log.Panic(err)

        }

        if b != nil {

            err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())

            if err != nil {

                log.Panic(err)

            }

            b.Put([]byte("l"), genesisBlock.Hash)

        }

        return nil

    })

    if err != nil {

        log.Panic(err)

    }

通过db.Upadate方法进行数据更新操作

创建/打开存储区块的Bucket:BlockBucketName

将创世区块序列化后存入Bucket中

通过Put方法更新K/V值(Key:区块哈希,Value:区块序列化后的字节数组)

通过Put方法更新Key为“l”的Value为最新区块哈希值,此处即genesisBlock.Hash

5.命令行调用

func (cli *CLI) CreateBlockChain(address string) {

    CreateBlockChainWithGenesisBlock(address)

}

测试命令

$ ./mybtc  createblockchain -address 1DHPNHKfk9uUdog2f2xBvx9dq4NxpF5Q4Q

返回结果

数据库不存在

正在创建创世区块

32325,00005c7b4246aa88bd1f9664c665d6424d1522f569d981691ac2b5b5d15dd8d9

本章节介绍了如何创建一个带有创世区块的区块链,并持久化存储至数据库blockchain.db

$ ls

BLC             Wallets.dat     blockchain.db   main.go         mybtc

©著作权归作者所有:来自51CTO博客作者暗黑魔君的原创作品,如需转载,请注明出处,否则将追究法律责任


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消