这两天一直在忙着去中心化交易所开发中,目前已经实现了btc和eth的跨连转账,预计年底就可以正式商用上线了,后续这个交易所将开源,我也打算出一个专辑,从0开始搭建去中心化的跨链数字资产交易所。今天继续翻译这个系列,不过今天看的时候,发现这个系列已经有中文翻译了,但是秉着学习的态度,自己继续翻把

原帖地址:

前言

目前为止,我们已经建立了基于工POW的区块链系统,这使得挖矿成为可能。我们的系统越来越接近真实的区块链系统,但是其仍旧缺乏一些重要特征。今天我们一起探讨区块链在数据库中的存储,并将我们的系统加上简单的命令行功能。本质上讲,区块链是一个分布式数据库,我们在这里忽略“分布式”,并将注意力放在“数据库”上。

数据库选型

我们的系统中暂时还没有数据库。我们在每次创建区块的时候将其放在内存里。我们还不能重利用区块链并和他人共享信息。因此,我们需要将其放在内存中。

我们需要哪个数据库?实际上,任意一个都OK。中本聪的论文并未涉及具体的数据库。因此,数据库的选型依赖于开发人员。中本聪发布的第一个btc版本 Bitcoin Core使用levelDB。我们使用BoltDB。

BoltDB

理由:

  1. 简单并且轻量级
  2. 使用golang开发.
  3. 不需要运行服务器
  4. 允许我们自定义数据结构
Bolt是受LMDB启发使用Golang实现的kv数据库。其目标是在不运行类似Postgre或者MYsql数据库的条件下提供一个简单,快速可靠的数据存储。

看起来满足我们的需要,我们再回顾下

BoltDB是一个kv存储, 这意味着其中没有关系数据表,数据是以kv对存储的。 kv被存储在桶中,桶是用来存储相似的kv对的。为了获取一个数据,需要知道桶和key。

bolt的一个重要特性是其没有数据类型:key和value是位矩阵。由于我们打算把区块存进去,我们需要对其进行序列化。这里使用 encoding/gob而不是json,xml,protobuf来处理。encoding/gob由go标准库提供。

数据结构

在开始执行持久化逻辑之前,我们首先确定我们的数据结构。我们首先看看btc是怎么做的。简言之,bitcoin core使用两个“桶”来存储数据:

(1)blocks存储描述区块的元数据

(2)chainstate存储链的状态,即所有未花费的输出和一些元数据

(1)Database Structure

此外,区块被分开存储在磁盘上的不同文件。这么做可以提高性能:读一个单独的区块不需要将许多区块加载入内存,我们的实现没那么复杂。

blocks
'b' +  -> 'f' + 4位文件号 -> 文件'l' -> : 'R' -> 1位bool: 我'F' + 1-位flag名字长度 + flag名字字符串 -> 1位bool: 多个't' + 32位的交易hash ->
chainstate
'c' + 32位的交易哈希->'B' -> 32位的块哈希: the block hash up to which the database represents the unspent transaction outputs

更多细节请看here

由于我们当前还没有交易,我们只有blocks桶。同时,如上所述,我们将整个数据库当作一个文件,并不对其分开存储。所以我们不需要相关的文件号。因此我们的kv对如下:

32-块hash ->序列化后的块结构 'l' -> 

我们已经讲完了持久化的逻辑。


序列化

如前所述,在boltDB中,数据使用[]byte表示,为了保存我们的区块,我们使用encoding/gob来序列化结构。如下所示(简单起见,我们忽略了错误处理):

上述代码段简单明了,首先,我们声明了一个buffer来存储序列化数据;然后初始化一个gob编码器来对整个块进行编码;最后将结果以位数组返回。

接下来,我们需要反序列化函数将位数组转成区块。如下所示:

目前位置,我们的序列化任务完成。

持久化

我们从NewBlockchain函数开始。它目前创建了一个新区块链,并加入创始区块到里面。我们要做的是:

1、打开一个数据库文件

2、检查数据库是否由一个区块链

3、如有,否则转4

(a)创建一个新区块实例

(b)将区块链实例的tip指向数据库中最后一个区块的hash值

4、(a)创建创始区块

(b)存于数据库

(c)将创始块的hash保存为最后一个hash

(d)创建一个新区块实例,将其tip指向创始块

代码如下:

一起看看这段代码:


首先打开一个boltDB,注意我们没有错误处理

在boltdb中,数据库操作以事务进行。并且有两类事务:可读的和读写。这里,我们使用读写事务,因为我们要将创始区块塞进去。

这是函数的核心。这里,我们得到了存储我们区块的桶:如果存在,我们从它读了l-key;如果不存在,我们创建创始区块,创建桶,将区块保存到桶里,更新l-key。

因此,注意到创建区块的新方式:

我们并不存储所有的blocks,只保存链的tip。同时,我们存储DB连接,因此我们只打开一次并一直运行它。区块结构如下:


借来我们更新AddBlock方法:添加区块就像添加一个元素到数组中。从现在开始我们将区块存到数据库中。

一起来看看:

这是bolt中的只读事务,我们从数据库中得到最后一个块的hash,并使用它来挖矿

在挖矿后,我们将其保存在数据库中更更新l-key
区块链不难把?

深入区块链

现在所有的区块都已经存到数据库了,我们在这里重新打开区块链并向其加入新的区块。但是我们仍然缺乏一项功能:我们不能把区块打印出来,因为我们不再把区块放进数组里。让我们来搞定这个问题!

boltdb允许迭代所有桶中的key,但是所有key按照位序存储,我们希望打印出来的区块按照其入块顺序。同时,我们不希望将所有的区块加载至内存。为了达到目的,我们在区块中加入迭代器。

当我们每次想迭代访问区块链的时候我们都创造一个迭代器,并保存当前迭代器的块hash和一个数据库的连接。因此,迭代器在逻辑上是区块链的一部分。迭代器的创建也是区块链的方法:

注意到迭代器初始指向区块链的tip,并且区块是以从top到bottom的方法获取的,即从最新到最旧。实际上,选择一个tip意味着为区块投票。一个区块链可以有多个分支,最长的链为主链。当我们得到区块链的tip时,我们可以重构整个链并且确定其长度并依此进行构建股权看来。即tip是区块链的标识符。


区块链的迭代器只做一件事:返回区块链的下一个区块。

数据库大功告成!

命令行

目前为止我们并未对程序生成任何接口:我们只是简单的在main函数里执行NewBlockchain,bc.AddBlock,是时间改进了,我们需要如下命令:

所有的命令行操作都由CLI结构体处理:


入口是run函数:

我们使用标准flag包解析命令参数:


addblockprintchain-dataprintchain


我们首先建立了两个子命令,addblock和printchain,然后为addblock加了data flag

接下来我们检查命令并解析flag

然后我们在相关函数中看看解析的是哪个命令

这段代码和之前的非常类似,差别仅仅在于使用迭代器遍历区块链

别忘了修改main函数

让我们看看运行情况:

下次我们将一起讨论钱包,地址和交易!敬请关注!