Introduction

到目前为止,我们已经建立了一个带有工作量证明系统的区块链,这使得挖矿成为可能。我们的实现越来越接近功能齐全的区块链,但它仍然缺乏一些重要的功能。今天将开始在数据库中存储区块链,之后我们将创建一个简单的命令行界面来执行区块链操作。从本质上讲,区块链是一个分布式数据库。我们暂时将省略“分布式”部分,并专注于“数据库”部分。

Database Choice

目前,我们的实现中没有数据库;相反,我们每次运行程序时都会创建块并将它们存储在内存中。我们无法重用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。

我们需要哪个数据库?实际上,任何一个数据库都可以。在the original Bitcoin paper没有任何关于使用某个数据库的说法,因此由开发人员决定使用哪个数据库。Bitcoin Core,最初由中本聪发布,目前是比特币的参考实现,使用LevelDB。而我们将要使用的是....

BoltDB

因为:

  1. 它足够简单。
  2. 它使用Go实现。
  3. 它不需要运行服务器。
  4. 它允许构建我们想要的数据结构。

Bolt是一个纯粹的Go键/值存储,受到Howard Chu的LMDB项目的启发。该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供简单,快速,可靠的数据库。

由于Bolt旨在用作这种低级功能,因此简单性是关键。 API很小,只关注获取值和设置值。而已。

听起来非常适合我们的需求!我们花点时间回顾一下。

BoltDB是一个键/值存储,这意味着没有像SQL RDBMS(MySQL,PostgreSQL等)中的表,没有行,没有列。相反,数据存储为键值对(如Golang映射中)。键值对存储在存储桶中,存储桶用于对类似的对进行分组(这类似于RDBMS中的表)。因此,为了获得一个值,您需要知道一个桶和一个密钥。

BoltDB的一个重要特点是没有数据类型:键和值是字节数组。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储Block(块)),我们需要对它们进行序列化,即实现一种将Go结构转换为字节数组并从字节数组中恢复的机制。我们会用的encoding/gob , 不过 JSON, XML, Protocol Buffers等也可以使用。我们正在使用encoding/gob因为它很简单,是标准Go库的一部分。

Database Structure

在开始实现持久性逻辑之前,我们首先需要决定如何在数据库中存储数据。为此,我们将参考比特币核心的方式。

简单来说,比特币核心使用两个“桶”来存储数据:

  1. blocks存储描述链中所有块的元数据。
  2. chainstate存储链的状态,这是当前未使用的事务输出和一些元数据。

此外,块作为单独的文件存储在磁盘上。这样做是出于性能目的:读取单个块不需要将所有(或部分)块加载到内存中。我们不会实现这一点。

blocks 中,key -> value 为:

key value
b + 32 字节的 block hash block index record
f + 4 字节的 file number file information record
l + 4 字节的 file number the last block file number used
R + 1 字节的 boolean 是否正在 reindex
F + 1 字节的 flag name length + flag name string 1 byte boolean: various flags that can be on or off
t + 32 字节的 transaction hash transaction index record

chainstatekey -> value 为:

key value
c + 32 字节的 transaction hash unspent transaction output record for that transaction
B 32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs

详情可见 这里

由于我们还没有交易,我们将只有blocks桶。另外,如上所述,我们将整个DB存储为单个文件,而不将块存储在单独的文件中。所以我们不需要任何与文件编号相关的内容。最终,我们会用到的键值对有:

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

这就是实现持久化机制所有需要了解的内容了。

Serialization

如前所述,在BoltDB中,值只能是[]byte类型,我们想存储Block数据库中的结构。我们会用的encoding/gob序列化结构。

让我们来实现 BlockSerialize 方法(为了简洁起见,此处略去了错误处理):

func (b *Block) Serialize() []byte {var result bytes.Bufferencoder := gob.NewEncoder(&result)err := encoder.Encode(b)return result.Bytes()
}
复制代码

这个部分很简单:首先,我们声明一个存储序列化数据的缓冲区;然后我们初始化一个gob编码器和编码块;结果以字节数组的形式返回。

接下来,我们需要一个反序列化函数,它将接收一个字节数组作为输入并返回一个Block。这不是一种方法,而是一种独立的功能:

func DeserializeBlock(d []byte) *Block {var block Blockdecoder := gob.NewDecoder(bytes.NewReader(d))err := decoder.Decode(&block)return &block
}
复制代码

这就是序列化!

Persistence

让我们从 NewBlockchain 函数开始。在之前的实现中,NewBlockchain 会创建一个新的 Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:

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

在代码中,它看起来像这样:

func NewBlockchain() *Blockchain {var tip []bytedb, 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})bc := Blockchain{tip, db}return &bc
}
复制代码

让我们一块一块地回顾一下。

db, err := bolt.Open(dbFile, 0600, nil)
复制代码

这是打开BoltDB文件的标准方法。请注意,如果没有此类文件,它将不会返回错误。

err = db.Update(func(tx *bolt.Tx) error {
...
})
复制代码

在BoltDB中,使用数据库的操作在事务中运行。有两种类型的事务:只读和读写。在这里,我们打开一个读写事务(db.Update(...)),因为我们希望将创世块放在DB中。

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"))
}
复制代码

这是该功能的核心。在这里,我们获取存储块的存储桶:如果存在,我们读取l键;如果它不存在,我们生成创世块,创建桶,将块保存到其中,并更新l密钥存储链的最后一个块哈希。

另外,注意创建 Blockchain 一个新的方式:

bc := Blockchain{tip, db}
复制代码

我们不再存储其中的所有块,而是仅存储链的尖端。此外,我们存储数据库连接,因为我们想要打开它一次并在程序运行时保持打开状态。就这样Blockchain结构现在看起来像这样:

type Blockchain struct {tip []bytedb  *bolt.DB
}
复制代码

接下来我们要更新的是AddBlock方法:现在向链中添加块并不像向数组中添加元素那么容易。从现在开始,我们将在数据库中存储块:

func (bc *Blockchain) AddBlock(data string) {var lastHash []byteerr := bc.db.View(func(tx *bolt.Tx) error {b := tx.Bucket([]byte(blocksBucket))lastHash = b.Get([]byte("l"))return nil})newBlock := NewBlock(data, lastHash)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.Hashreturn nil})
}
复制代码

让我们一块一块地回顾一下:

err := bc.db.View(func(tx *bolt.Tx) error {b := tx.Bucket([]byte(blocksBucket))lastHash = b.Get([]byte("l"))return nil
})
复制代码

这是BoltDB事务的另一种(只读)类型。在这里,我们从DB获取最后一个块哈希,以使用它来挖掘新的块哈希。

newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
复制代码

在挖掘新块之后,我们将其序列化表示保存到DB中并更新l键,现在存储新块的哈希值。

完成!这不难,是吗?

Inspecting Blockchain

所有新块现在都保存在数据库中,因此我们可以重新打开区块链并向其添加新块。但是在实现之后,我们失去了一个很好的功能:我们不能再打印出区块链块了,因为我们不再将块存储在数组中。让我们解决这个缺陷吧!

BoltDB允许迭代桶中的所有键,但键按字节排序顺序存储,我们希望块按照它们在区块链中的顺序打印。另外,因为我们不想将所有块加载到内存中(我们的区块链数据库可能很大!或者只是假装它可以),我们将逐一阅读它们。为此,我们需要一个区块链迭代器:

type BlockchainIterator struct {currentHash []bytedb          *bolt.DB
}
复制代码

每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希(currentHash)和数据库的连接(db)。通过 db,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:

func (bc *Blockchain) Iterator() *BlockchainIterator {bci := &BlockchainIterator{bc.tip, bc.db}return bci
}
复制代码

请注意,迭代器最初指向区块链的顶端,因此将从上到下,从最新到最旧获得块。事实上,选择一个tip意味着对区块链进行投票。区块链可以有多个分支,并且它们中最长的被认为是主要分支。获得tip后(它可以是区块链中的任何块)我们可以重建整个区块链并找到它的长度和构建它所需的工作。这同样也意味着,一个 tip 也就是区块链的一种标识符。

BlockchainIterator 只会做一件事情:返回链中的下一个块。

func (i *BlockchainIterator) Next() *Block {var block *Blockerr := 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.PrevBlockHashreturn block
}
复制代码

这就是数据库部分!

CLI

到目前为止,我们的实现还没有提供任何与程序交互的接口:我们只是执行了NewBlockchainbc.AddBlock。是时候改善了!我们想要这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain复制代码

所有与命令行相关的操作都将由CLI struct处理:

type CLI struct {bc *Blockchain
}
复制代码

它的 “入口” 是 Run 函数:

func (cli *CLI) Run() {cli.validateArgs()addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)addBlockData := addBlockCmd.String("data", "", "Block data")switch os.Args[1] {case "addblock":err := addBlockCmd.Parse(os.Args[2:])case "printchain":err := printChainCmd.Parse(os.Args[2:])default:cli.printUsage()os.Exit(1)}if addBlockCmd.Parsed() {if *addBlockData == "" {addBlockCmd.Usage()os.Exit(1)}cli.addBlock(*addBlockData)}if printChainCmd.Parsed() {cli.printChain()}
}
复制代码

我们正在使用该标准flag包解析命令行参数。

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
复制代码

首先,我们创建两个子命令: addblockprintchain, 然后给 addblock 添加 -data 标志。printchain 没有任何标志。

switch os.Args[1] {
case "addblock":err := addBlockCmd.Parse(os.Args[2:])
case "printchain":err := printChainCmd.Parse(os.Args[2:])
default:cli.printUsage()os.Exit(1)
}
复制代码

然后,我们检查用户提供的命令,解析相关的 flag 子命令:

if addBlockCmd.Parsed() {if *addBlockData == "" {addBlockCmd.Usage()os.Exit(1)}cli.addBlock(*addBlockData)
}if printChainCmd.Parsed() {cli.printChain()
}
复制代码

接着检查解析是哪一个子命令,并调用相关函数:

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("Prev. 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}}
}
复制代码

这件作品与我们之前的作品非常相似。唯一的区别是我们现在正在使用BlockchainIterator迭代区块链中的块。

记得不要忘了对 main 函数作出相应的修改:

func main() {bc := NewBlockchain()defer bc.db.Close()cli := CLI{bc}cli.Run()
}复制代码

注意,无论提供什么命令行参数,都会创建一个新的链。

这就是今天的所有内容了! 来看一下是不是如期工作:

$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109bPrev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13Success!$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148Success!$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: truePrev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: truePrev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
复制代码

英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/

更多文章欢迎访问 http://www.apexyun.com/

联系邮箱:public@space-explore.com

(未经同意,请勿转载)

使用Go构建区块链 第3部分:持久化和cli相关推荐

  1. 基于Java语言构建区块链(四)—— 交易(UTXO)

    基于Java语言构建区块链(四)-- 交易(UTXO) 2018年03月11日 00:48:01 wangwei_hz 阅读数:909 标签: 区块链比特币 更多 个人分类: 区块链 文章的主要思想和 ...

  2. 基于Java语言构建区块链(五)—— 地址(钱包)

    基于Java语言构建区块链(五)-- 地址(钱包) 2018年03月25日 18:02:06 wangwei_hz 阅读数:1292更多 个人分类: 区块链bitcoin比特币 文章的主要思想和内容均 ...

  3. 基于Java语言构建区块链(六)—— 交易(Merkle Tree)

    基于Java语言构建区块链(六)-- 交易(Merkle Tree) 2018年04月16日 10:21:35 wangwei_hz 阅读数:480更多 个人分类: 区块链比特币bitcoin 最终内 ...

  4. fcn从头开始_如何使用Go从头开始构建区块链

    fcn从头开始 介绍 (Introduction) With Web 3.0 and blockchain becoming more mainstream every day, do you kno ...

  5. 基于Go语言构建区块链:part5

    Golang语言和区块链理论学习完毕后,快速入门方法无疑是项目实战.本文将参考https://jeiwan.cc/tags/blockchain/教程,学习如何基于Go语言构建区块链. 1.引言 本文 ...

  6. 银心科技与黑萤科技达成战略合作,联合构建区块链数据库存储生态至高点

    银心科技与黑萤科技达成战略合作,联合构建区块链数据库存储生态至高点 元为开始,旦为天明,2019年元月,分布式存储领域利好不断,先是海外知名项目.全球首个区块链数据库存储ORA甲骨链在熊市逆势获得千万 ...

  7. 从构建区块链理解区块链概念

    从构建区块链理解区块链概念 import hashlib import json from datetime import time from urllib.parse import urlparse ...

  8. 用Python构建区块链

    区块链 区块链是在计算机网络的节点之间共享数据的分类账(分布式数据库).作为数据库,区块链以电子格式储存信息.区块链的创新之处在于它保证了数据记录的安全性和真实性,可信性(不需要没有可信任的第三方). ...

  9. 通过JavaScript学习构建区块链

    通过JavaScript学习构建区块链 [中英双语]通过JavaScript 学习构建区块链 用 JavaScript 编程语言编写您自己的区块链和去中心化网络. 此教程共8.0小时,中英双语字幕,画 ...

  10. 软硬件一体提高主链性能,「HPB芯链」想构建区块链版的云计算

    软硬件一体提高主链性能,「HPB芯链」想构建区块链版的云计算 HPB芯链是一个区块链软硬件体系架构,其中包含芯片加速引擎和区块链底层平台. 底层设施的不完善限制了区块链行业的发展,在公链层面,目前主要 ...

最新文章

  1. Centos的网络配置命令和文件
  2. Winform开发框架之肖像显示保存控件的实现
  3. win7电脑恢复系统设置或计算机点不了,Win7系统电脑开机就提示需要启动修复的解决方法...
  4. 【计算机网络】数据链路层 : 差错控制 ( 检错编码 | 奇偶校验码 | CRC 循环冗余码 )★
  5. 强化学习(三)---马尔科夫决策过程
  6. leetCode刷题第一天--求两数之和
  7. 第13次预习课-20180919 多进程编程
  8. Spring boot 通过ApplicationRunner在启动完成后按指定顺序执行任务
  9. aws lambda_AWS API Gateway和AWS Lambda示例
  10. plaintextedit指定一行一行的高亮显示_RDKit | 基于RDKit的指定原子或键高亮
  11. 基于麻雀算法改进的LSTM分类算法-附代码
  12. Composite_组合模式_PHP语言描述
  13. URLDecoder
  14. 白帽子讲Web安全——世界观安全
  15. 几种流行Webservice框架
  16. libcef-框架架构中概念介绍-命令行参数-元素布局-应用程序结构(二)
  17. Piranha web 界面LVS DR 模式配置图文详解
  18. 吴恩达 (Andrew Ng) 是一个怎样的人
  19. AJAX初窥门径教程
  20. 用python绘制熊猫图案_使用熊猫在Python中绘制数据

热门文章

  1. Cocos文档案例游戏设计的梳理与分析
  2. centos7.4 安装配置rabbitmq-server 实操记录
  3. 20165237 2017-2018-2 《Java程序设计》第8周学习总结
  4. Struts2 标签
  5. android 系统相册调用,各版本的区别总结
  6. key-list类型内存数据引擎介绍及使用场景
  7. 会计基础(1)利得和损失
  8. 使用 JavaScript 生成二维码 —— QRCode.js
  9. 科研不是比赛,而是一种对未知和完美的自我追求——跟邢波(Eric Xing)面对面聊科研...
  10. Ubuntu 16.04 安装 Apache, MySQL, PHP7