介绍
到现在,我们已经构建了一个有工作量证明系统和可以挖矿的区块链。我们的实现离一个具有完整功能的区块链又进了一步,但是它仍然少一些重要的性质。今天,我们要将区块链存储到数据库中,在这之后,我们将做一个简单的命令行接口来支持区块链的操作。在它的一个重要的本质中,区块链是一个分布式数据库;我们现在要省略“分布式”的部分,而集中精力于“数据存储”部分。
数据库的选择
目前,在我们的实现中并没有数据库;反而我们在每一次运行程序创建区块时都存储在内存中。我们既不能重复使用区块链也不能与其他人共享,因此我们需要将它存储在磁盘中。
我们需要哪一个数据库呢?事实上,任何一个都可以。在比特币的原始论文中,没有说明使用哪一个特定的数据库。所以,使用什么数据库完全取决于开发者。 Bitcoin Core ,最初由中本聪发布的,现在是比特币的一个参考实现,它使用的是LevelDB(虽然它在2012年才发布客户端)。而我们将要使用的是…
BoltDB
原因:
- 它非常简单便捷。
- 它是由Go语言实现的。
- 它不需要运行任何一个服务器。
- 它允许我们想要的数据结构。
在__BoltDB__ Github上的README写道:
Bolt 是一个纯键值存储的 Go 数据库,启发自 Howard Chu 的 LMDB. 它旨在为那些无须一个像 Postgres 和 MySQL 这样有着完整数据库服务器的项目,提供一个简单,快速和可靠的数据库。
由于 Bolt 意在用于提供一些底层功能,简洁便成为其关键所在。它的 API 并不多,并且仅关注值的获取和设置。仅此而已。
听起来很完美的契合了我们的需求!让我们快速的回顾一下它:
__BoltDB__是一个键值存储结构,这就意味着它不像关系型数据库(MySQL、PostgreSQL等)有表,没有行、列。相反的,数据以一种key-value(键值对)的组合形式存储(就像Go语言中的__map__结构)。键值对存储在桶中,这是有意的给键值对分组(这有些像关系型数据库中的表)。因此,为了获得一个值,你需要知道一个桶(bucket)和一个键(key)。
关于__BoltDB__一个重要的点是它没有数据类型:键和值都是以字节数组的形式存储。由于我们要存储Go的数据结构(具体来说是__Block__),我们需要将它们进行序列化。也就是说,实现一个可以将Go结构体转化为字节数组并可以从字节数组恢复到Go结构体的机制。我们将使用encoding/gob来实现这个,但是JSON、XML、Protocol Buffers等也是可以的。我们使用encoding/gob是因为它简单而且是Go的标准库。
数据库结构
在开始实现持久化的逻辑之前,我们需要决定怎样将数据存储到数据库中。为此,我们参考比特币的做法。
简单来说,比特币使用了两个桶(bucket)来存储数据:
- __blocks__存储链上区块的元数据描述。
- __chainstate__存储链的状态,也就是目前所有的未花费交易输出以及一些数据。
同时,区块被存储为磁盘上不同的文件。出于对性能的考虑:加载单个的区块不需要从内存中加载所有(或者部分)文件。我们不需要实现这些。
在 blocks 中, key -> value 对应关系是这样的:
- ‘b’ + 32 字节的区块 hash -> 区块索引记录
- ‘f’ + 4 字节文件数字 -> 文件信息记录
- ‘l’ -> 4 字节文件数字: 最后一个使用过的区块文件数字
- ‘R’ -> 1 字节布尔值: 我们是否要去重新索引
- ‘F’ + 1 字节标志名长度 + 标志名 -> 1 字节布尔值: 开或关的多种标志
- ‘t’ + 32 字节交易 hash -> 交易索引记录
在 chainstate, key -> value 对应关系是这样的:
- ‘c’ + 32 字节交易 hash -> 未使用的交易出账记录
- ‘B’ -> 32 字节区块 hash: 数据库应该表示的未使用交易出账的区块哈希
(更详细的解释可以在这里找到)
因为我们现在还不需要交易,我们只需要有一个__blocks__桶就可以。同时,就像前面说的,我们将以单个文件的形式存储在数据库中,不需要在分开的文件中存储区块。所以我们也不需要任何与文件相关的数字编号。因此,我们只需要这些键值的对应关系:
- 32 字节区块 hash -> 区块数据(序列化后的)
- ‘l’ -> 链上最后一个区块的 hash
以上,是我们在实现持久化之前所需要知道的全部。
序列化
如前文所说,在__BoltDB__中数据只能是__[]byte__的形式,我们想存储__Block__结构在数据库中。我们将使用encoding/gob来对结构体进行序列化。
让我们实现区块的__Serialize__方法(为了简洁,我们暂时忽略错误处理):
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
这个模块非常直接了当:首先,我们为所要序列化的数据定义了一个buffer来存储;然后我们初始化了一个__gob encoder__并且对区块进行编码;结果以一个字节数组的形式返回。
接下来,我们需要一个反序列化的函数接受输入的字节数组并返回一个__Block__区块。这不是一个方法而是一个独立的函数:
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
这就是反序列化了!
持久化
让我们开始__NewBlockchain__函数。目前,它创建一个新的__Blockchain__并且添加一个创世纪块进去。我们希望它可以做:
- 打开一个数据库文件。
- 检查里面是否以及存在一个区块链在里面。
- 如果这里已经有了一个区块链:
- 创建一个新的区块链实例。
- 设置这个区块链实例的__tip__为数据库中最后一个区块的哈希。
代码中,它看起来像:
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("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__中,数据库通过一个事务进行操作。这里有两种事务:只读(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"(字母L小写,不是1);如果不存在,我们就生成一个创世纪块,创建一个桶,将区块保存进去,之后更新"l"键,使其存储区块链的最后一个区块的哈希。
同时,注意一下创建__Blockchain__的新方法:
bc := Blockchain{tip, db}
我们不再存储所有的区块,取而代之的是仅仅存储区块链的__tip__。同时我们也保存一个数据库连接,因为我们想只打开它一次,并且让它在程序运行的过程中一直保持着连接。所以,__Blockchain__结构看起来就像这样:
type Blockchain struct {
tip []byte
db *bolt.DB
}
接下来我们想做的就是更新__AddBlock__方法:现在添加区块到区块链上不是简单的向数组中添加一个元素了。从现在开始我们将把区块存储到数据库中:
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
err := 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.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__数据库的另一种事务:只读(read-only)。这里我们从数据库中得到了最后一个区块的哈希,用来挖新的区块哈希。
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
在挖出新区块之后,我们将其序列化特征值存储到数据库中,并且更新"l"键,让它保存最新区块的哈希。
完成了!并不是很难,对吗!
检查区块链
所有的新区块现在保存在数据库中,所以我们现在可以重新打开这条链并且向其中添加新的区块。但是在实现了这些后,我们失去了一个非常好的特性:我们不能打印区块链中的区块了,因为我们不再像以前那样存储区块了。让我们修复这个瑕疵!
__BoltDB__数据库允许对桶里的所有key进行迭代,但是所有的key都以字节顺序存储,我们又想以区块在区块链中的顺序进行打印。而且,因为我们不想加载内存中所有的区块(我们的区块链存储数据可能非常庞大!或者假装是这样),我们将把它们一个一个读出来。为此,我们需要一个区块链迭代器:
type BlockchainIterator struct {
currentHash []byte
db *bolt.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 *Block
err := 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.PrevBlockHash
return 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__,然后我们添加__-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"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Prev. 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"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Success!
$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
Success!
$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true
Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
Github的译者版本附加的测试:
(看起来可以开酒(🍷or🍺)了)
总结
接下来我们将实现地址、钱包以及交易(或许也有)。尽情期待!
Links: