前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >教你打造最简比特币之持久化

教你打造最简比特币之持久化

作者头像
linxinzhe
发布2018-04-10 15:13:20
8820
发布2018-04-10 15:13:20
举报
文章被收录于专栏:林欣哲
开发环境:Go语言

本教程是学习Jeiwan的博客后的学习笔记,代码实现也参考它的为主,精简了叙述并在适当位置添加了一些必备的小知识和适当的代码注释,如介绍哈希。

本教程是为了逐步教你设计一款简化的区块链原型币。通过我们不断添加功能,完成一个可交易的原型币。

本节我们增加持久化的功能,可以持久化区块链到本地文件。

  1. 单机版,仅支持保存信息✅
  2. 工作量证明✅
  3. 持久化

选择数据库

目前,我们的原型币是存储在内存里的,每次运行结束后消失。而真正的比特币账本是需要持久化保存到本地的,比特币的核心开发者们,选择的是 LevelDB,是一个键值存储的数据库,类似的,我们也选取一个键值对存储的数据库,他是轻量级的、用Go语言实现BoltDB

BoltDB

他的数据存取都是用的键,可以当作一个Map来使用。 需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。因此,对于一些Go语言的结构化数据如struct,我们使用标准库encoding/gob来做转换。

数据库的结构设计

比特币的实现使用了两个数据库(在键值对数据库中称为“bucket”)存储区块链。

  1. “block”,存储了描述一条链中所有块的元数据
  2. “chainstate”,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据。

值得注意的是,比特币为了节约内存,将每个区块(block)作为一个文件存储为磁盘上。 我们为了简单实现,会在一个文件包含全部区块链。

我们会用到的键值对有:

  1. 32 字节的 block-hash -> block 结构
  2. l -> 链中最后一个块的 hash
序列化和反序列化

将我们的Block结构体,序列化为[]byte,以及把[]byte反序列化为Block结构体

序列化代码如下:

代码语言:javascript
复制
func (b *Block) Serialize() []byte {
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}

反序列化代码如下:

代码语言:javascript
复制
func DeserializeBlock(d []byte) *Block {
    var block Block

    decoder := gob.NewDecoder(bytes.NewReader(d))
    err := decoder.Decode(&block)

    return &block
}

如果搞不清Go语言的读写数据的代码,可参考这篇文章Go语言标准库 的第一章

区块链的持久化

首先,改变我们只抢Blockchain的struct结构,

代码语言:javascript
复制
type Blockchain struct {
    Tip []byte  //表示最后一个区块的[]byte表示
    DB  *bolt.DB //数据库的指针
}

注意这里的改动,将原先的[]*Block去掉,因为原先相当于把全部Block读取出来放入了内存,如果数据量大则会爆内存。要节约内存,只能保存一个数据库的指针,每次根据需要去数据库里查出来对应的区块。这里用tip存储最新的区块的hash。

创建区块链的算法如下:

  1. 打开一个数据库文件
  2. 检查文件里面是否已经存储了一个区块链
  3. 如果已经存储了一个区块链:
    1. 创建一个新的 Blockchain 实例
    2. 设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
  4. 如果没有区块链:
    1. 创建创世块
    2. 存储到数据库
    3. 将创世块哈希保存为最后一个块的哈希
    4. 创建一个新的 Blockchain 实例,初始时 tip 指向创世块(tip 有尾部,尖端的意思,在这里 tip 存储的是最后一个块的哈希)

算法实现的代码大概是这样:

代码语言:javascript
复制
func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)

    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))

        if b == nil {
            genesis := NewGenesisBlock()
            b, err := tx.CreateBucket([]byte(blocksBucket))
            err = b.Put(genesis.Hash, genesis.Serialize())
            err = b.Put([]byte("l"), genesis.Hash)
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }

        return nil
    })  //注意看这里是丢了一个匿名函数进去,让BoltDB去执行update

    bc := Blockchain{tip, db}

    return &bc
}

接下来更新添加区块的方法,算法是:

  1. 查库取出最新区块的hash。
  2. 挖矿新区块。
  3. 再添加回数据库
代码语言:javascript
复制
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"))

        return nil
    })   

    newBlock := NewBlock(data, lastHash)  //这里不查库,直接传bc.tip也可以,因为他只需最后一个区块的hash。既然你能取到blockchain的db了,相当于也就tip有值即最后一个区块的hash

    err = bc.DB.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        err := b.Put(newBlock.Hash, newBlock.Serialize())
        err = b.Put([]byte("l"), newBlock.Hash)
        bc.tip = newBlock.Hash

        return nil
    })
}

至此,我们的区块链持久化已经成功,但是目前有个缺点,就是不再能直接遍历区块链了。

遍历区块链

笨办法是全部读取到内存,但是这样就违背了我们设计Blockchain的结构的初衷。 因此我们要用迭代器来,逐个逐个的从数据库取数据。

代码语言:javascript
复制
type BlockchainIterator struct {
    CurrentHash []byte
    DB          *bolt.DB
}

然后给Blockchain结构加上迭代器

代码语言:javascript
复制
func (bc *Blockchain) Iterator() *BlockchainIterator {
    bci := &BlockchainIterator{bc.Tip, bc.DB}

    return bci
}

并实现Next接口

代码语言:javascript
复制
func (i *BlockchainIterator) Next() *Block {
    var block *Block

    err := i.DB.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        encodedBlock := b.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)

        return nil
    })

    i.CurrentHash = block.PrevHash //为下一个查库作准备

    return block
}

最后,让我们打印一下这个区块链。

代码语言:javascript
复制
func printChain(blockchain *Blockchain) {
    bci := blockchain.Iterator()

    for {
        block := bci.Next()

        fmt.Printf("Prev. hash: %x\n", block.PrevHash)
        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.PrevHash) == 0 {
            break
        }
    }
}

调用main函数如下

代码语言:javascript
复制
func main() {
    blockchain := NewBlockchain()
    blockchain.AddBlock("Block1: 1 BTC to Lin")
    blockchain.AddBlock("Block2: 2 BTC to Lin")

    printChain(blockchain)
}

记得每次删除db重置数据库哦!

控制台

如果你希望便利的通过控制台直接指挥程序做事,可以通过Go语言的标准库flag实现。代码如下:

代码语言:javascript
复制
type CLI struct {
    Blockchain *Blockchain
}

func (cli *CLI) run() {
    if len(os.Args) < 2 {//控制台,当前运行的文件名是os.Args[0],还需要一个参数才可以执行指令
        flag.Usage()
        return
    }

    addBlockCmd := flag.NewFlagSet("addBlock", flag.ExitOnError)
    addBlockMsg := addBlockCmd.String("data", "", "Your block message") //通过控制台输入“-data xxxx”,得到一个xxx字符串的addBlockMsg填入区块
    printChainCmd := flag.NewFlagSet("printChain", flag.ExitOnError)

    switch os.Args[1] {
    case "addBlock":
        addBlockCmd.Parse(os.Args[2:])//flag帮助解析,-data后的指令
    case "printChain":
        printChainCmd.Parse(os.Args[2:])
    default:
        flag.Usage()
        return
    }

    if addBlockCmd.Parsed() { //如果用了addBlock指令,则只抢的Parse调用后,该指令的Paresed为真
        if *addBlockMsg == "" {
            addBlockCmd.Usage()
            return
        } //Usage会帮助打印使用说明
        addBlock(cli.Blockchain, *addBlockMsg)
    }

    if printChainCmd.Parsed() {
        printChain(cli.Blockchain)
    }
}

func addBlock(blockchain *Blockchain, data string) {
    blockchain.AddBlock(data)
}

参考:

Building Blockchain in Go. Part 3: Persistence and CLI,jiewan

源码

https://github.com/linxinzhe/go-simple-coin/tree/3_persistence

下一节:

全系列:

  1. 区块链原形币--工作量证明
  2. 区块链原型币—工作量证明
  3. 区块链原型币—持久化
  4. 未完待续
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 林欣哲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 选择数据库
    • BoltDB
      • 序列化和反序列化
  • 数据库的结构设计
  • 区块链的持久化
  • 遍历区块链
  • 控制台
    • 参考:
      • 源码
        • 下一节:
          • 全系列:
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档