用 golang 实现区块链系列三 | 持久化与命令行

简介

到现在为止,我们已经搞了一个带有工作量证明的区块链,它使得挖矿成为可能。我们的实现已经离一个功能全面的区块链更近了一步,但仍然缺少一些重要的功能。今天我们会开始吧区块链存在一个数据库里,然后做一个简单的命令行接口投操作区块链。本质上,区块链是一个分布式的数据库。我们先省略“分布式”这个部分,集中处理“数据库”这部分。

数据库选择

当前,我们的视线中没有数据库;作为替代,我们每次运行程序会创建区块并存在内存里。我们不能重复使用区块链,我们不能和其他人共享数据,所以我们需要把它存在硬盘上。

我们需要哪种数据库?事实上,一个都不需要。在 比特币论文的原文 中,从来没说过要用哪种依存数据库,所以开发者高兴用哪个就用哪个。 比特币核心,就那个中本聪最初发布的版本,也是目前比特币实现的参考版本,使用的是 LevelDB (尽管在 2012 年才给客户公布出来),所以咱也用这个。

BoltDB

因为:

  1. 他很简单而且很小
  2. 它用 Go 写的
  3. 它不需要跑一个服务器
  4. 它允许我们去创建我们自己想要的数据结构

下面来自于 BlotDB 在 github 上的 README 写的:

Blot 是一个用纯 Go 写的键值存储,受 Howard Chu 的 LMDB 项目启发而做的。本项目的目标是给并不需要类似于 Postgres 或者 MySQL 这样完整数据库服务的项目提供一个简单的,快速的,可靠地数据库。

由于 Blot 是用来做底层功能的,简单是关键。API 就很小而且只集中于写数据和读数据,仅此而已。

听起来就是我们需要的!花点儿时间再去审核一下这个东西。

BoltDB 是一个键值存储,这就意味着没有像是 SQL 关系型数据库(像是 MySQL, PostgresQL 等)的表结构,没有行,没有列。取而代之的是数据以键值配对(像是 Go 语言里的 map),键值配对被存储在桶(bucket)中。这样就可以集合相似的匹配关系(类似于关系型数据库里面的表结构),这样,为了获得值,我们就需要一个桶和一个键(key)

BoltDB 还有一个重要的特性就是它没有数据结构:键和值都是二进制数组。因为我们需要存储 Go 数据结构(特指区块)到数据库里,我们需要序列化数据。我们需要实现一种机制去转换 Go 的数据结构到 byte 数组还要从 byte 数组里把它恢复出来。我们使用 encoding/gob 做这个事情。 不过JSONXMLProtocol Buffers也都行。我们用 encoding/gob 是因为它很简单,而且是 Go 语言标准库的一部分。

数据库结构

在实现持久化逻辑之前,我们得首先去决定如何在数据库里保存数据。因为这个事情,我们得去参考一下比特币核心怎么做的。

简而言之,比特币核心用了两个桶(bucket)去保存数据:

  1. blocks 保存链上所有区块的元信息
  2. chainstate 保存链的状态,它是所有当前未完成的交易出账和一些元信息。

同时,区块被分散地存储在硬盘上,这是为了性能烤炉:读单个的区块并不需要加载其中所有(或者一部分)到内存里。我们不会实现这个部分。

在 blocks 中, key -> value 对应关系是这样的:

  1. ‘b’ + 32 字节的区块 hash -> 区块索引记录
  2. ‘f’ + 4 字节文件数字 -> 文件信息记录
  3. ‘l’ -> 4 字节文件数字: 最后一个使用过的区块文件数字
  4. ‘R’ -> 1 字节布尔值: 我们是否要去重新索引
  5. ‘F’ + 1 字节标志名长度 + 标志名 -> 1 字节布尔值: 开或关的多种标志
  6. ‘t’ + 32 字节交易 hash -> 交易索引记录

在 chainstatekey -> value 对应关系是这样的:

  1. ‘c’ + 32 字节交易 hash -> 未使用的交易出账记录
  2. ‘B’ -> 32 字节区块 hash: 数据库应该表示的未使用交易出账的区块哈希

(更详细的解释可以在这里找到)

因为我们暂时并没有交易信息,我们可以只有一个 blocks 桶。所以,像上面说的那样,我们将会把整个数据库存成单文件,不会分成几个文件。这样我们就不需要任何关于文件号的信息了。这样,键值对应关系就会成这样:

  1. 32 字节区块 hash -> 区块数据(序列化后的)
  2. ‘l’ -> 链上最后一个区块的 hash

这些就是我们开始实现持久化机制所应知道的所有东西了

序列化

就像前面说的那样,在 BoltDB 值只能是 []byte 类型,那么我们想存储 Block 结构到数据库里,我们需要用 encoding/gob 去序列化数据结构。

现在来给 Block 实现一个 Serialize 方法吧(为了简略,错误处理就省略了):

func (b *Block) Serialize() []byte {
   
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)

    err := encoder.Encode(b)

    return result.Bytes()
}

这一块很简洁:我们申明了一个将会存储序列化后结构的 buffer;随后出赤化 gob 编码器并对区块进行编码;最后把字节数组作为结果返回出去。

然后,我们需要反序列化的方法,它要接受一个字节数组作为入账,并且返回一个区块。这不会是一个方法看,而是一个独立函数:

func DeserializeBlock(d []byte) *Block {
   
    var block Block

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

    return &block
}

这就是序列化要做的事情啦!

持久化

让我们从 NewBlockchain 函数开始。 现在,它创建一个 Blockchain 的实例,并添加一个创始区块到里面。我们想做的是这样的:

  1. 打开数据库文件
  2. 确认是否有可以复原的区块链信息
  3. 如果有区块链的话:1. 创建一个区块链实例。2. 设定区块链实例恢复到 数据库里的最后一个区块 hash。
  4. 如果没有已存在的区块链: 1. 创建创始区块。2. 存入数据库。3. 存储创始区块的 hash 作为最后一个区块 hash。 5. 创建一个新的区块链实例,指向创始区块

代码上像是这样:

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 {
   
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值