当前位置:   article > 正文

使用go语言构建区块链 Part3.持久化与命令行接口_go结构体持久化

go结构体持久化

英文源地址

简介

到目前为止, 我们已经建立了一个带有工作量证明系统的区块链, 这使得挖矿成为可能. 我们的实现越来越接近于一个功能齐全的区块链了. 但它仍然缺乏一些重要的功能. 今天我们将开始再数据库中存储区块链, 之后我们将实现一个简单的命令行接口来执行区块链的操作. 从本质上讲, 区块链是一个分布式数据库. 我们将忽略’分布式’的部分, 而专注于’数据库’的部分.

数据库的选择

目前, 我们的实现中没有数据库; 取而代之的, 我们在每次运行程序时创建区块并将它们存储在内存中. 我们不能重复使用区块链, 我们不能与他人共享之, 因此我们需要将其存储在磁盘上.
我们需要哪个数据库呢?实际上, 任何一个都可以. 在最初的比特币论文中, 没有提到使用某个数据库, 所以使用哪种数据库取决于开发人员.BitcoinCore最初由中本聪(Satoshi Nakamoto)发布, 目前是比特币的参考实现, 使用LevelDB(尽管它在2012年才被引入客户端). 而我们会使用的是…

BoltDB

因为:

  1. 它简单而且简约
  2. 它是用Go语言实现的
  3. 它不需要运行服务端
  4. 它允许构建我们想要的数据结构

从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使用两个’桶’来存储数据:

  1. blocks区块, 存储链中所有区块的元数据
  2. chainstate, 存储链的状态, 即当前所有未使用的事务输出和一些元数据

此外, 区块作为单独的文件存储在磁盘上. 这样做是出于性能考虑: 读取单个块不需要将它们全部(或部分)加载到内存中. 我们不会实现这个.
在区块中, 键-值对是:

  1. ‘b’+32字节区块散列值->区块索引记录
  2. ‘f’+4字节文件号->文件信息记录
  3. ‘l’->4字节文件号: 最后使用的区块文件号
  4. ‘R’->1字节布尔值: 我们是否正在重新索引
  5. ‘F’+1字节标志名长度+标志名字符串->1字节布尔值: 可以打开或关闭的各种状态
  6. ‘t’+32字节的事务哈希值->事务索引记录

在chainstate中, key->value对是:

  1. ‘c’+32字节的事务哈希值->该事务的未使用事务输出记录
  2. ‘B’->32字节区块哈希: 数据库表示未使用事务输出的区块哈希值

详细的描述可以看这里

因为我们还没有事务, 所以我们只有blocks桶. 此外, 如上所述, 我们将整个DB存储为单个文件, 而不将区块存储在单独的文件中. 所以我们不需要任何与文件编号相关的东西. 所以这些是我们将使用的key-value对:

  1. 32字节的Block-hash->区块结构(序列化)
  2. ‘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()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这部分很简单: 首先, 声明一个缓冲区来存储序列化的数据, 然后初始化一个gob编码器并对区块进行编码, 结果以字节数组的形式返回.
接下来, 我们需要一个反序列化函数, 它将接收一个字节数组作为输入并返回一个Block. 这将不是一个方法, 而是一个独立的函数.

func DeserializeBlock(d []byte) *Block {
	var block Block

	decoder := gob.NewDecoder(bytes.NewReader(d))
	_ = decoder.Decode(&block)
	return &block
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这就是序列化操作的全部!

持久化

让我们从NewBlockchain函数开始. 目前, 它创建了一个新的区块链实例, 并将创世区块添加到其中. 我们想让它做的是:

  1. 打开一个DB文件
  2. 检查其中存储的区块链
  3. 如果存在有区块链
    1.创建一个新的Blockchain实例
    2.将Blockchain实例的顶端设置为数据库中存储的最后一个区块的哈希值
  4. 如果没有现有的区块链:
    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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

让我们一块一块来看.

db, err := bolt.Open(dbFile, 0600, nil)
  • 1

这是打开BoltDB文件的标准方法.注意, 如果没有这样的文件, 它不会返回错误.

err = db.Update(func(tx *bolt.Tx) error {
...
})
  • 1
  • 2
  • 3

在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"))
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这是函数的核心. 在这里, 我们获得存储区块的桶:如果它存在, 我们从它读取l键; 如果它不存在, 我们生成创世区块, 创建桶, 将区块保存到桶中, 并更新存储链的最后一个区块哈希值的l键.
另外, 注意创建区块链的新方法:

bc := Blockchain{tip, db}
  • 1

我们不再存储所有的区块, 而是只存储链的末端. 此外, 我们还存储了一个DB连接, 因为我们希望打开它一次, 并在程序运行时保持打开状态. 因此, 区块链结构现在看起来像这样:

type Blockchain struct {
	tip []byte
	db *bolt.DB
}
  • 1
  • 2
  • 3
  • 4

我们想要更新的下一个方法是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
	})
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

让我们一部分一部分看

err := bc.db.View(func(tx *bolt.Tx) error {
	b := tx.Bucket([]byte(blocksBucket))
	lastHash = b.Get([]byte("l"))

	return nil
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这是另一种(只读)类型的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
  • 1
  • 2
  • 3
  • 4
  • 5

在挖到新区块后, 我们将其序列化表示保存到DB中并更新键, 该键现在存储新区块的哈希值.
完成了!这并不难, 对吧?

检查区块链

现在所有的新区快都保存在数据库中, 所以我们可以重新打开区块链并向其添加新区块.但是在实现这个之后, 我们失去了一个很好的特性: 我们不能再打印区块链的区块了, 因为我们不再将区块存储在数组中. 让我们来修复这个缺陷!
BoltDB允许遍历一个桶中的所有key, 但key是按字节序的顺序存储的, 我们希望区块按照它们在区块链中的顺序打印. 此外, 因为我们不想将所有区块加载到内存中(我们的区块链数据库可以很大!或者让我们假装它可以很大), 我们将一个一个读取它们. 为此, 我们需要一个区块链迭代器:

type BlockchainIterator struct {
	currentHash []byte
	db          *bolt.DB
}
  • 1
  • 2
  • 3
  • 4

每次我们想要迭代区块链中的区块时, 都会创建一个迭代器, 它将存储当前迭代的区块哈希值以及DB的连接. 由于后者的存在, 一个迭代器在逻辑上附属到区块链上(它时存储DB连接的Blockchain实例), 因此, 在Blockchain方法中创建:

func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip, bc.db}
	
	return bci
}
  • 1
  • 2
  • 3
  • 4
  • 5

请注意, 迭代器最初指向区块链的顶端, 因此将自上而下, 从最新的区块到最旧的区块进行获取.事实上, 选择一个顶端区块意味着为区块链’投票’. 一个区块链可以有多个分支, 其中最长的分支被认为是主分支. 在得到一个顶端的区块(它可以是区块链的任何区块)之后, 我们可以重建整个区块链并找到它的长度和构建它需要的工作. 这一事实也意味着顶端区块是区块链的一个标识符.
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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这就是DB部分!

CLI

到目前为止, 我们的实现还没有提供任何与程序交互的接口: 我们只是在main函数中, 执行了NewBlockchain, bc.AddBlock.是时候改进它了!我们希望有这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain
  • 1
  • 2

所有与命令行相关的操作都将由CLI结构体处理

type CLI struct {
	bc *Blockchain
}
  • 1
  • 2
  • 3

它的入口是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()
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

我们使用标准库中的flag包解析命令行参数.

addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
  • 1
  • 2
  • 3

首先, 我们创建两个子命令, 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)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

接下来, 我们校验用户提供的命令并解析相关的flag子命令.

if addBlockCmd.Parsed() {
	if *addBlockData == "" {
		addBlockCmd.Usage()
		os.Exit(1)
	}
	cli.addBlock(*addBlockData)
}

if printChainCmd.Parsed() {
	cli.printChain()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

接下来, 我们检查解析了哪些子命令并运行了相关的函数.

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
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这部分与我们之前写的一部分很像. 唯一的区别是, 我们现在使用BlockchainIterator来迭代区块链中的区块.
我们也不要忘记相应地修改main函数:

func main() {
	bc := NewBlockchain()

	defer bc.db.Close()

	cli := CLI{bc}
	cli.Run()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

请注意, 无论提供什么命令行参数, 都会创建一个新的区块链.
就是这样!让我们检查一下是否一切正常:

╰─ ./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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

(打开啤酒罐的声音)

总结

下次我们将实现: 地址, 钱包和(可能会有)事务.敬请期待!

Links
Full source codes
Bitcoin Core数据存储
boltdb
encoding/gob
flag

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/931475
推荐阅读
相关标签
  

闽ICP备14008679号