赞
踩
到目前为止, 我们已经建立了一个带有工作量证明系统的区块链, 这使得挖矿成为可能. 我们的实现越来越接近于一个功能齐全的区块链了. 但它仍然缺乏一些重要的功能. 今天我们将开始再数据库中存储区块链, 之后我们将实现一个简单的命令行接口来执行区块链的操作. 从本质上讲, 区块链是一个分布式数据库. 我们将忽略’分布式’的部分, 而专注于’数据库’的部分.
目前, 我们的实现中没有数据库; 取而代之的, 我们在每次运行程序时创建区块并将它们存储在内存中. 我们不能重复使用区块链, 我们不能与他人共享之, 因此我们需要将其存储在磁盘上.
我们需要哪个数据库呢?实际上, 任何一个都可以. 在最初的比特币论文中, 没有提到使用某个数据库, 所以使用哪种数据库取决于开发人员.BitcoinCore最初由中本聪(Satoshi Nakamoto)发布, 目前是比特币的参考实现, 使用LevelDB(尽管它在2012年才被引入客户端). 而我们会使用的是…
因为:
从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使用两个’桶’来存储数据:
此外, 区块作为单独的文件存储在磁盘上. 这样做是出于性能考虑: 读取单个块不需要将它们全部(或部分)加载到内存中. 我们不会实现这个.
在区块中, 键-值对是:
在chainstate中, key->value对是:
因为我们还没有事务, 所以我们只有blocks桶. 此外, 如上所述, 我们将整个DB存储为单个文件, 而不将区块存储在单独的文件中. 所以我们不需要任何与文件编号相关的东西. 所以这些是我们将使用的key-value对:
这是开始实现持久化机制所需要知道的全部内容.
如前所述, 在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函数开始. 目前, 它创建了一个新的区块链实例, 并将创世区块添加到其中. 我们想让它做的是:
在代码中, 它看起来像这样:
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部分!
到目前为止, 我们的实现还没有提供任何与程序交互的接口: 我们只是在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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。