用 Go 构建一个区块链 -- Part 3: 持久化和命令行接口
翻译的系列文章我已经放到了 GitHub 上:blockchain-tutorial,后续如有更新都会在 GitHub 上,可能就不在这里同步了。如果想直接运行代码,也可以 clone GitHub 上的教程仓库,进入 src 目录执行 make
即可。
引言
到目前为止,我们已经构建了一个有工作量证明机制的区块链。有了工作量证明,挖矿也就有了着落。虽然目前的实现离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性。在今天的内容中,我们会将区块链持久化到一个数据库中,然后会提供一个简单的命令行接口,用来完成一些与区块链的交互操作。本质上,区块链是一个分布式数据库,不过,我们暂时先忽略 “分布式” 这个部分,仅专注于 “存储” 这一点。
选择数据库
目前,我们的区块链实现里面并没有用到数据库,而是在每次运行程序时,简单地将区块链存储在内存中。那么一旦程序退出,所有的内容就都消失了。我们没有办法再次使用这条链,也没有办法与其他人共享,所以我们需要把它存储到磁盘上。
那么,我们要用哪个数据库呢?实际上,任何一个数据库都可以。在 比特币原始论文 中,并没有提到要使用哪一个具体的数据库,它完全取决于开发者如何选择。 Bitcoin Core ,最初由中本聪发布,现在是比特币的一个参考实现,它使用的是 LevelDB。而我们将要使用的是...
BoltDB
因为它:
- 非常简单和简约
- 用 Go 实现
- 不需要运行一个服务器
- 能够允许我们构造想要的数据结构
BoltDB GitHub 上的 README 是这么说的:
Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。
由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的
API 并不多,并且仅关注值的获取和设置。仅此而已。
听起来跟我们的需求完美契合!来快速过一下:
Bolt 使用键值存储,这意味着它没有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,没有行和列。相反,数据被存储为键值对(key-value pair,就像 Golang 的 map)。键值对被存储在 bucket 中,这是为了将相似的键值对进行分组(类似 RDBMS 中的表格)。因此,为了获取一个值,你需要知道一个 bucket 和一个键(key)。
需要注意的一个事情是,Bolt 数据库没有数据类型:键和值都是字节数组(byte array)。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储(块)Block),我们需要对它们进行序列化,也就说,实现一个从 Go struct 转换到一个 byte array 的机制,同时还可以从一个 byte array 再转换回 Go struct。虽然我们将会使用 encoding/gob 来完成这一目标,但实际上也可以选择使用 JSON, XML, Protocol Buffers 等等。之所以选择使用 encoding/gob, 是因为它很简单,而且是 Go 标准库的一部分。
数据库结构
在开始实现持久化的逻辑之前,我们首先需要决定到底要如何在数据库中进行存储。为此,我们可以参考 Bitcoin Core 的做法:
简单来说,Bitcoin Core 使用两个 “bucket” 来存储数据:
- 其中一个 bucket 是 blocks,它存储了描述一条链中所有块的元数据
- 另一个 bucket 是 chainstate,存储了一条链的状态,也就是当前所有的未花费的交易输出,和一些元数据
此外,出于性能的考虑,Bitcoin Core 将每个区块(block)存储为磁盘上的不同文件。如此一来,就不需要仅仅为了读取一个单一的块而将所有(或者部分)的块都加载到内存中。但是,为了简单起见,我们并不会实现这一点。
在 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 |
在 chainstate,key -> 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 |
详情可见 这里:_Data_Storage)。
因为目前还没有交易,所以我们只需要 blocks bucket。另外,正如上面提到的,我们会将整个数据库存储为单个文件,而不是将区块存储在不同的文件中。所以,我们也不会需要文件编号(file number)相关的东西。最终,我们会用到的键值对有:
- 32 字节的 block-hash -> block 结构
l
-> 链中最后一个块的 hash
这就是实现持久化机制所有需要了解的内容了。
序列化
上面提到,在 BoltDB 中,值只能是 []byte
类型,但是我们想要存储 Block
结构。所以,我们需要使用 encoding/gob 来对这些结构进行序列化。
让我们来实现 Block
的 Serialize
方法(为了简洁起见,此处略去了错误处理):
func (b *Block) Serialize() []byte {var result bytes.Bufferencoder := gob.NewEncoder(&result)err := encoder.Encode(b)return result.Bytes()
}
这个部分比较直观:首先,我们定义一个 buffer 存储序列化之后的数据。然后,我们初始化一个 gob
encoder 并对 block 进行编码,结果作为一个字节数组返回。
接下来,我们需要一个解序列化的函数,它会接受一个字节数组作为输入,并返回一个 Block
. 它不是一个方法(method),而是一个单独的函数(function):
func DeserializeBlock(d []byte) *Block {var block Blockdecoder := gob.NewDecoder(bytes.NewReader(d))err := decoder.Decode(&block)return &block
}
这就是序列化部分的内容了。
持久化
让我们从 NewBlockchain
函数开始。在之前的实现中,它会创建一个新的
Blockchain
实例,并向其中加入创世块。而现在,我们希望它做的事情有:
- 打开一个数据库文件
- 检查文件里面是否已经存储了一个区块链
如果已经存储了一个区块链:
- 创建一个新的
Blockchain
实例 - 设置
Blockchain
实例的 tip 为数据库中存储的最后一个块的哈希
- 创建一个新的
如果没有区块链:
- 创建创世块
- 存储到数据库
- 将创世块哈希保存为最后一个块的哈希
- 创建一个新的
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 中,数据库操作通过一个事务(transaction)进行操作。有两种类型的事务:只读(read-only)和读写(read-write)。这里,打开的是一个读写事务(db.Update(...)
),因为我们可能会向数据库中添加创世块。
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"))
}
这里是函数的核心。在这里,我们先获取了存储区块的 bucket:如果存在,就从中读取 l
键;如果不存在,就生成创世块,创建 bucket,并将区块保存到里面,然后更新 l
键以存储链中最后一个块的哈希。
另外,注意创建 Blockchain
一个新的方式:
bc := Blockchain{tip, db}
这次,我们不在里面存储所有的区块了,而是仅存储区块链的 tip。另外,我们存储了一个数据库连接。因为我们想要一旦打开它的话,就让它一直运行,直到程序运行结束。因此,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 事务的另一个类型(只读)。在这里,我们会从数据库中获取最后一个块的哈希,然后用它来挖出一个新的块的哈希:
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
检查区块链
现在,产生的所有块都会被保存到一个数据库里面,所以我们可以重新打开一个链,然后向里面加入新块。但是在实现这一点后,我们失去了之前一个非常好的特性:我们再也无法打印区块链的区块了,因为现在不是将区块存储在一个数组,而是放到了数据库里面。让我们来解决这个问题!
BoltDB 允许对一个 bucket 里面的所有 key 进行迭代,但是所有的 key 都以字节序进行存储,而且我们想要以区块能够进入区块链中的顺序进行打印。此外,因为我们不想将所有的块都加载到内存中(因为我们的区块链数据库可能很大!或者现在可以假装它可能很大),我们将会一个一个地读取它们。故而,我们需要一个区块链迭代器(BlockchainIterator):
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 (可以是链中的任意一个块)之后,我们就可以重新构造整条链,找到它的长度和需要构建它的工作。这同样也意味着,一个 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
到目前为止,我们的实现还没有提供一个与程序交互的接口:目前只是在 main
函数中简单执行了 NewBlockchain
和 bc.AddBlock
。是时候改变了!现在我们想要拥有这些命令:
blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain
所有命令行相关的操作都会通过 CLI
结构进行处理:
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")
首先,我们创建两个子命令: addblock
和 printchain
, 然后给 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
总结
在下篇文章中,我们将会实现地址,钱包,(可能实现)交易。尽请收听!
链接
- Full source codes
- Bitcoin Core Data Storage:_Data_Storage)
- boltdb
- encoding/gob
- flag
本文源码:part_3
原文:Building Blockchain in Go. Part 3: Persistence and CLI
用 Go 构建一个区块链 -- Part 3: 持久化和命令行接口相关推荐
- 通过python构建一个区块链来学习区块链
了解区块链Blockchains如何工作的最快方法就是构建一个区块链.你来到这里是因为,和我一样,你对加密钱币的崛起感到很兴奋.而且你想知道区块链是如何工作的,想了解它们背后的基本技术. 但理解区块链 ...
- php构建一个区块链(含源码)
php构建一个区块链(含源码) 我们要用PHP编程语言构建区块链,区块链本身就是一个非常简单的概念,它是一个非常简单的数据结构,数字货币是很复杂,但区块链不是,它们复杂的原因是共识算法,挖矿机制和运行 ...
- 用 Go 构建一个区块链 -- Part 7: 网络
翻译的系列文章我已经放到了 GitHub 上:blockchain-tutorial,后续如有更新都会在 GitHub 上,可能就不在这里同步了.如果想直接运行代码,也可以 clone GitHub ...
- 用 Go 构建一个区块链 -- Part 6: 交易(2)
翻译的系列文章我已经放到了 GitHub 上:blockchain-tutorial,后续如有更新都会在 GitHub 上,可能就不在这里同步了.如果想直接运行代码,也可以 clone GitHub ...
- 用 Go 构建一个区块链 ---- Part 1: 基本原型
翻译的系列文章我已经放到了 GitHub 上:blockchain-tutorial,后续如有更新都会在 GitHub 上,可能就不在这里同步了.如果想直接运行代码,也可以 clone GitHub ...
- 用 Go 构建一个区块链 -- Part 1: 基本原型
引言 区块链是 21 世纪最具革命性的技术之一,它仍然处于不断成长的阶段,而且还有很多潜力尚未显现出来. 本质上,区块链只是一个分布式数据库而已. 不过,使它独一无二的是,区块链是一个公开的数据库,而 ...
- 【区块链新手快速入门】如何构建一个区块链
本文翻译自<Learn Blockchains by Building One>,作者@dvf,原文链接:https://hackernoon.com/learn-blockchains- ...
- 用Go语言建立一个简单的区块链part3:持久化和命令行接口
持久化和命令行接口 引言 到目前为止,我们已经构建了一个有工作量证明机制的区块链.有了工作量证明,挖矿也就有了着落.虽然目前距离一个有着完整功能的区块链越来越近了,但是它仍然缺少了一些重要的特性.在今 ...
- 200行代码构建一个区块链
区块链的基本概念非常简单:一个存储不断增加的有序记录的分布式数据库.然而,当我们谈论区块链时,我们很容易将其与区块链要解决的问题混淆,比如误解为流行的,基于区块链的,像比特币和以太坊一样的项目.术语& ...
最新文章
- AGAGA XOOORRR CodeForces - 1516B
- 数据结构课上笔记12
- python按钮代码_Python QPalette.Button方法代码示例
- bzoj5118: Fib数列2(费马小定理+矩阵快速幂)
- 【华为2019年校园招聘】2019-5-8 软件题
- html5都能干嘛种花,把这19种花种起来,整个家都能变成大花园!
- 江诗丹顿 VACHERON CONSTANTIN
- 配色工具Material Design
- 直观理解Dilated Convolution
- CSDN黑科技----简单暴力
- Win11色温如何进行调整设置
- C#/VB.NET 在Excel单元格中应用多种字体格式
- 那些情况该使用它们spin_lock到spin_lock_irqsave
- 详解Docker的基本原理与实践操作
- 软件测试行业的优缺点
- 如何做一个好的BI总监
- SAP官网学习教程(2)创建HANA数据库
- 前端性能优化篇——DNS/CDN
- linux nginx的 yum安装
- mongodb.cfg