介绍
到目前为止,我们已经建立了一个工作证明系统的区块链,这使得挖掘成为可能。 我们的实现越来越接近功能完整的区块链,但它仍然缺乏一些重要功能。 今天将开始在数据库中存储区块链,之后我们将制作一个简单的命令行界面来执行区块链操作。 其本质上,区块链是一个分布式数据库。 现在我们将省略“分布式”部分,并专注于“数据库”部分。
数据库选择
目前,我们的实施中没有数据库; 相反,我们每次运行程序时都会创建块并将它们存储在内存中。 我们不能重复使用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。
我们需要哪个数据库? 其实,他们中的任何一个。 在[最初的比特币文件中] ,关于使用某个数据库没有任何说法,因此开发人员需要使用哪个DB。 [Bitcoin Core]最初由Satoshi Nakamoto发布,目前是比特币的参考实现,它使用[LevelDB](尽管它仅在2012年引入客户端)。 我们将使用...
BoltDB
因为:
它简单而简约。
它在Go中实现。
它不需要运行服务器。
它允许构建我们想要的数据结构。
从[Github上]
Bolt是一个纯粹的Go键/价值商店,受到了Howard Chu的LMDB项目的启发。 该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供一个简单,快速且可靠的数据库。
由于Bolt旨在用作这种低级功能,因此简单性是关键。 该API将很小,只专注于获取值和设置值。 而已。
听起来非常适合我们的需求! 让我们花一分钟审查一下。
BoltDB是一个键/值存储,这意味着没有像SQL RDBMS(MySQL,PostgreSQL等)那样的表,没有行,没有列。 相反,数据存储为键值对(如Golang地图中)。 键值对存储在桶中,用于对类似的对进行分组(这与RDBMS中的表类似)。 因此,为了获得价值,你需要知道一个水桶和一把钥匙。
关于BoltDB的一个重要的事情是没有数据类型:键和值是字节数组。 由于我们将Go结构(特别是Block
)存储在其中,因此我们需要序列化它们,即实现将Go结构转换为字节数组并将其从字节数组恢复的机制。 我们将为此使用[编码/ gob],但也可以使用JSON
, XML
, Protocol Buffers
等。 我们使用encoding/gob
因为它很简单,并且是标准Go库的一部分。
数据库结构
在开始实施持久性逻辑之前,我们首先需要决定如何将数据存储在数据库中。 为此,我们将介绍比特币核心的做法。
简而言之,Bitcoin Core使用两个“存储桶”来存储数据:
-
blocks
存储描述链中所有blocks
元数据。 -
chainstate
存储chainstate
的状态,目前所有链接都是未使用的事务输出和一些元数据。
另外,块在磁盘上作为单独的文件存储。 这是为了达到性能目的而完成的:读取单个块不需要将全部(或部分)全部加载到内存中。 我们不会执行这个。
在blocks
, key -> value
对是:
'b' + 32-byte block hash -> block index record
'f' + 4-byte file number -> file information record
'l' -> 4-byte file number: the last block file number used
'R' -> 1-byte boolean: whether we're in the process of reindexing
'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
't' + 32-byte transaction hash -> transaction index record
在chainstate
, key -> value
对是:
'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
(详细解释可以在[这里]找到)
由于我们还没有交易,因此我们只会blocks
存货桶。 另外,如上所述,我们将整个DB存储为单个文件,而不将块存储在单独的文件中。 所以我们不需要任何与文件编号有关的东西。 因此,这些是我们将使用的key -> value
对:
32-byte block-hash -> Block structure (serialized)
'l' -> the hash of the last block in a chain
这就是我们开始实施持久性机制所需要知道的。
序列化
如前所述,BoltDB中的值只能是[]byte
类型,并且我们希望将Block
结构存储在数据库中。 我们将使用[编码/ gob]来序列化结构。
让我们来实现Block
的Serialize
方法(为简洁起见省略了错误处理):
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
这篇文章很简单:首先,我们声明一个缓冲区,它将存储序列化的数据; 然后我们初始化一个gob编码器并编码该块; 结果以字节数组的形式返回。
接下来,我们需要一个反序列化函数,它将接收一个字节数组作为输入并返回一个Block 。 这不是一个方法,而是一个独立的功能:
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
这就是序列化!
坚持
我们从NewBlockchain函数开始。 目前,它创建了一个新的Blockchain实例,并为其添加了生成块。 我们希望它做的是:
打开一个数据库文件。
检查是否存在区块链。
如果有区块链:
创建一个新的Blockchain实例。
将Blockchain实例的顶端设置为存储在数据库中的最后一个块哈希。
如果不存在区块链:
创建起始块。
存储在数据库中。
将创建块的散列保存为最后一个块散列。
创建一个新的Blockchain实例,其尖端指向创世区块。
在代码中,它看起来像这样:
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中,数据库操作在事务中运行。 有两种类型的事务:只读和读写。 在这里,我们打开一个读写事务( 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"))
}
这是该功能的核心。 在这里,我们获得存储块的桶:如果它存在,我们从它读取l键; 如果它不存在,我们生成生成块,创建桶,将块保存到其中,并更新存储链的最后块哈希的l密钥。
另外,请注意创建Blockchain的新方法:
bc := Blockchain{tip, db}
我们不再存储所有的块,而只是存储链的顶端。 另外,我们存储一个数据库连接,因为我们想打开它一次,并在程序运行时保持打开状态。 因此, Blockchain结构现在看起来像这样:
type Blockchain struct {
tip []byte
db *bolt.DB
}
接下来我们要更新的是AddBlock方法:现在向链中添加块并不像将元素添加到数组中那么容易。 从现在开始,我们将块存储在DB中:
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事务。 在这里,我们从数据库中获取最后一个块哈希来使用它来挖掘一个新的块哈希。
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允许迭代桶中的所有键,但键以字节排序的顺序存储,我们希望块按照它们在区块链中的顺序进行打印。 另外,因为我们不想将所有块加载到内存中(我们的区块链数据库可能很大,或者我们假装它可以),我们将逐个读取它们。 为此,我们需要一个区块链迭代器:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
每次我们想要遍历区块链中的块时,都会创建一个迭代器,它将存储当前迭代的块散列和到数据库的连接。 由于后者,迭代器在逻辑上连接到Blockchain链(它是一个存储数据库连接的Blockchain实例),因此,它是在Blockchain方法中创建的:
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
请注意,迭代器最初指向区块链的顶端,因此块将从上到下,从最新到最旧获取。 事实上, 选择提示意味着区块链的“投票” 。 区块链可以有多个分支,并且它们中被认为是最长的分支。 获得小费后(可以是区块链中的任何区块),我们可以重新构建整个区块链,并查找其长度以及构建区块链所需的工作。 这个事实也意味着提示是一种区块链的标识符。
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 ,然后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 。
就是这样! 让我们来检查一切是否按预期工作:
$ 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
(啤酒的声音可以打开)
结论
下次我们将实施地址,钱包和(可能)交易。 敬请期待!