简介
到目前为止, 我们已经建立了一个带有工作量证明系统的区块链, 这使得挖矿成为可能. 我们的实现越来越接近于一个功能齐全的区块链了. 但它仍然缺乏一些重要的功能. 今天我们将开始再数据库中存储区块链, 之后我们将实现一个简单的命令行接口来执行区块链的操作. 从本质上讲, 区块链是一个分布式数据库. 我们将忽略’分布式’的部分, 而专注于’数据库’的部分.
数据库的选择
目前, 我们的实现中没有数据库; 取而代之的, 我们在每次运行程序时创建区块并将它们存储在内存中. 我们不能重复使用区块链, 我们不能与他人共享之, 因此我们需要将其存储在磁盘上.
我们需要哪个数据库呢?实际上, 任何一个都可以. 在最初的比特币论文中, 没有提到使用某个数据库, 所以使用哪种数据库取决于开发人员.BitcoinCore最初由中本聪(Satoshi Nakamoto)发布, 目前是比特币的参考实现, 使用LevelDB(尽管它在2012年才被引入客户端). 而我们会使用的是…
BoltDB
因为:
- 它简单而且简约
- 它是用Go语言实现的
- 它不需要运行服务端
- 它允许构建我们想要的数据结构
从BoltDB在github上的README:
Bolt是一个纯Go键值对存储, 灵感来自Howard Chu的LMDB项目.该项目的目标是为不需要完整数据库服务端(如Postgre或MySQL)的项目提供一个简单,快速和可靠的数据库.
由于Bolt是作为一个低级功能使用的, 因此简单是关键.API将很小, 并且只关注于获取值和设置值.就是这样
听起来完全符合我们的需求!我们花点时间复习一下.
BoltDB是一个kv存储, 这意味着没有像SQL RDBMS(MySQL, Postgre, etc)等关系型数据库管理系统那样的表, 没有行, 没有列. 取而代之的, 数据被存储为键值对(就像Golang中的字典一样). 键值对存储在桶中,桶用于对相似的对进行分组(这类似于RDBMS中的表) 因此, 为了获得一个值, 您需要直到一个bucket和一个key.
BoltDB的一个重要特性是没有数据类型: 键和值都是字节数组. 由于我们将在其中存储Go结构体(特别是Block). 我们需要序列化它们, 即实现将Go结构体转换为字节数组并将其从字节数组中恢复回来的机制.我们将使用encoding/gob, 但也可以使用json,xml,Protocal Buffers等.我们使用encoding/gob是因为它很简单, 并且是Go标准库的一部分.
数据库结构
在开始实现持久化逻辑之前, 我们首先需要决定如何在DB中存储数据. 为此, 我们将参考Bitcoin Core的实现方式.
简而言之, Bitcoin Core使用两个’桶’来存储数据:
- blocks区块, 存储链中所有区块的元数据
- chainstate, 存储链的状态, 即当前所有未使用的事务输出和一些元数据
此外, 区块作为单独的文件存储在磁盘上. 这样做是出于性能考虑: 读取单个块不需要将它们全部(或部分)加载到内存中. 我们不会实现这个.
在区块中, 键-值对是:
- ‘b’+32字节区块散列值->区块索引记录
- ‘f’+4字节文件号->文件信息记录
- ‘l’->4字节文件号: 最后使用的区块文件号
- ‘R’->1字节布尔值: 我们是否正在重新索引
- ‘F’+1字节标志名长度+标志名字符串->1字节布尔值: 可以打开或关闭的各种状态
- ‘t’+32字节的事务哈希值->事务索引记录
在chainstate中, key->value对是:
- ‘c’+32字节的事务哈希值->该事务的未使用事务输出记录
- ‘B’->32字节区块哈希: 数据库表示未使用事务输出的区块哈希值
因为我们还没有事务, 所以我们只有blocks桶. 此外, 如上所述, 我们将整个DB存储为单个文件, 而不将区块存储在单独的文件中. 所以我们不需要任何与文件编号相关的东西. 所以这些是我们将使用的key-value对:
- 32字节的Block-hash->区块结构(序列化)
- ‘l’->链中最后一个区块的哈希值
这是开始实现持久化机制所需要知道的全部内容.
序列化
如前所述, 在BoltDB中, 值只能是[]byte类型的, 并且我们希望在DB中存储Block结构体.我们将使用encoding/gob来序列化这些结构体.
让我们实现Block的序列化方法Serialize(为了简介, 省略了错误处理)
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
if err != nil {
return nil
}
return result.Bytes()
}
这部分很简单: 首先, 声明一个缓冲区来存储序列化的数据, 然后初始化一个gob编码器并对区块进行编码, 结果以字节数组的形式返回.
接下来, 我们需要一个反序列化函数, 它将接收一个字节数组作为输入并返回一个Block. 这将不是一个方法, 而是一个独立的函数.
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
_ = decoder.Decode(&block)
return &block
}
这就是序列化操作的全部!
持久化
让我们从NewBlockchain函数开始. 目前, 它创建了一个新的区块链实例, 并将创世区块添加到其中. 我们想让它做的是:
- 打开一个DB文件
- 检查其中存储的区块链
- 如果存在有区块链
1.创建一个新的Blockchain实例
2.将Blockchain实例的顶端设置为数据库中存储的最后一个区块的哈希值 - 如果没有现有的区块链:
1.创建创世区块
2.存储在DB中
3.将创世区块的哈希值保存为最后一个哈希值
4.创建一个新的区块链实例, 其顶端指向创世区块.
在代码中, 它看起来像这样:
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("1"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("1"))
}
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键.
另外, 注意创建区块链的新方法:
bc := Blockchain{tip, db}
我们不再存储所有的区块, 而是只存储链的末端. 此外, 我们还存储了一个DB连接, 因为我们希望打开它一次, 并在程序运行时保持打开状态. 因此, 区块链结构现在看起来像这样:
type Blockchain struct {
tip []byte
db *bolt.DB
}
我们想要更新的下一个方法是AddBlock: 现在向链中添加块不像向数组中添加元素那么简单. 从现在开始, 我们将在数据库中存储区块:
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
_ = 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.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
_ = b.Put(newBlock.Hash, newBlock.Serialize())
_ = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return 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
在挖到新区块后, 我们将其序列化表示保存到DB中并更新键, 该键现在存储新区块的哈希值.
完成了!这并不难, 对吧?
检查区块链
现在所有的新区快都保存在数据库中, 所以我们可以重新打开区块链并向其添加新区块.但是在实现这个之后, 我们失去了一个很好的特性: 我们不能再打印区块链的区块了, 因为我们不再将区块存储在数组中. 让我们来修复这个缺陷!
BoltDB允许遍历一个桶中的所有key, 但key是按字节序的顺序存储的, 我们希望区块按照它们在区块链中的顺序打印. 此外, 因为我们不想将所有区块加载到内存中(我们的区块链数据库可以很大!或者让我们假装它可以很大), 我们将一个一个读取它们. 为此, 我们需要一个区块链迭代器:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
每次我们想要迭代区块链中的区块时, 都会创建一个迭代器, 它将存储当前迭代的区块哈希值以及DB的连接. 由于后者的存在, 一个迭代器在逻辑上附属到区块链上(它时存储DB连接的Blockchain实例), 因此, 在Blockchain方法中创建:
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
请注意, 迭代器最初指向区块链的顶端, 因此将自上而下, 从最新的区块到最旧的区块进行获取.事实上, 选择一个顶端区块意味着为区块链’投票’. 一个区块链可以有多个分支, 其中最长的分支被认为是主分支. 在得到一个顶端的区块(它可以是区块链的任何区块)之后, 我们可以重建整个区块链并找到它的长度和构建它需要的工作. 这一事实也意味着顶端区块是区块链的一个标识符.
BlockchainIterator只会做一件事: 它会从区块链中返回下一个区块.
func (i *BlockchainIterator) Next() *Block {
var block *Block
_ = i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
encodeBlock := b.Get(i.currentHash)
block = DeserializeBlock(encodeBlock)
return nil
})
i.currentHash = block.PrevBlockHash
return block
}
这就是DB部分!
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":
_ = addBlockCmd.Parse(os.Args[2:])
case "printchain":
_ = 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, 然后在前者中添加-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.Print("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()
}
请注意, 无论提供什么命令行参数, 都会创建一个新的区块链.
就是这样!让我们检查一下是否一切正常:
╰─ ./main printchain ─╯
Prev. hash:
Data: Genesis Block
Hash: 000000af3d84a4798bfe67ced5cd779e63bad34351cf7d5624db731d9a88d55c
PoW: true
╰─ ./main addblock -data "Send 1 BTC to Ivan" ─╯
Mining the block containing "Send 1 BTC to Ivan"
000000a14750159cf16bd4e80ea05a2f75818bdfefd17de2c4f7a222fbd5ba1f
Success!
╰─ ./main addblock -data "Pay 0.31337 BTC for a coffee" ─╯
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000b2ddc5a69a640004db0370476a78012b0ad5d6c8e57d53cef4c6cba8c8
Success!
╰─ ./main printchain ─╯
Prev. hash: 000000a14750159cf16bd4e80ea05a2f75818bdfefd17de2c4f7a222fbd5ba1f
Data: Pay 0.31337 BTC for a coffee
Hash: 000000b2ddc5a69a640004db0370476a78012b0ad5d6c8e57d53cef4c6cba8c8
PoW: true
Prev. hash: 000000af3d84a4798bfe67ced5cd779e63bad34351cf7d5624db731d9a88d55c
Data: Send 1 BTC to Ivan
Hash: 000000a14750159cf16bd4e80ea05a2f75818bdfefd17de2c4f7a222fbd5ba1f
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 000000af3d84a4798bfe67ced5cd779e63bad34351cf7d5624db731d9a88d55c
PoW: true
(打开啤酒罐的声音)
总结
下次我们将实现: 地址, 钱包和(可能会有)事务.敬请期待!
Links
Full source codes
Bitcoin Core数据存储
boltdb
encoding/gob
flag