使用Go构建区块链 第3部分:持久化和cli

栏目: 数据库 · 发布时间: 5年前

内容简介:到目前为止,我们已经建立了一个带有工作量证明系统的区块链,这使得挖矿成为可能。我们的实现越来越接近功能齐全的区块链,但它仍然缺乏一些重要的功能。今天将开始在数据库中存储区块链,之后我们将创建一个简单的命令行界面来执行区块链操作。从本质上讲,区块链是一个分布式数据库。我们暂时将省略“分布式”部分,并专注于“数据库”部分。目前,我们的实现中没有数据库;相反,我们每次运行程序时都会创建块并将它们存储在内存中。我们无法重用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。我们需要哪个数据库?实际上,任何一

Introduction

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

Database Choice

目前,我们的实现中没有数据库;相反,我们每次运行程序时都会创建块并将它们存储在内存中。我们无法重用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。

我们需要哪个数据库?实际上,任何一个数据库都可以。在 the original Bitcoin paper 没有任何关于使用某个数据库的说法,因此由开发人员决定使用哪个数据库。 Bitcoin Core ,最初由中本聪发布,目前是比特币的参考实现,使用 LevelDB 。而我们将要使用的是....

BoltDB

因为:

  1. 它足够简单。
  2. 它使用 Go 实现。
  3. 它不需要运行服务器。
  4. 它允许构建我们想要的数据结构。

Bolt是一个纯粹的Go键/值存储,受到Howard Chu的LMDB项目的启发。该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供简单,快速,可靠的数据库。

由于Bolt旨在用作这种低级功能,因此简单性是关键。 API很小,只关注获取值和设置值。而已。

听起来非常适合我们的需求!我们花点时间回顾一下。

BoltDB是一个键/值存储,这意味着没有像SQL RDBMS(MySQL,PostgreSQL等)中的表,没有行,没有列。相反,数据存储为键值对(如Golang映射中)。键值对存储在存储桶中,存储桶用于对类似的对进行分组(这类似于RDBMS中的表)。因此,为了获得一个值,您需要知道一个桶和一个密钥。

BoltDB的一个重要特点是没有数据类型:键和值是字节数组。鉴于需要在里面存储 Go 的结构(准确来说,也就是存储 Block(块) ),我们需要对它们进行序列化,即实现一种将Go结构转换为字节数组并从字节数组中恢复的机制。我们会用的 encoding/gob , 不过 JSON, XML, Protocol Buffers等也可以使用。我们正在使用encoding/gob因为它很简单,是标准Go库的一部分。

Database Structure

在开始实现持久性逻辑之前,我们首先需要决定如何在数据库中存储数据。为此,我们将参考比特币核心的方式。

简单来说,比特币核心使用两个“桶”来存储数据:

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

此外,块作为单独的文件存储在磁盘上。这样做是出于性能目的:读取单个块不需要将所有(或部分)块加载到内存中。我们不会实现这一点。

blocks 中, key -> value 为:

keyvalue b + 32 字节的 block hashblock index record f + 4 字节的 file numberfile information record l + 4 字节的 file numberthe last block file number used R + 1 字节的 boolean是否正在 reindex F + 1 字节的 flag name length + flag name string1 byte boolean: various flags that can be on or off t + 32 字节的 transaction hashtransaction index record

chainstatekey -> value 为:

keyvalue c + 32 字节的 transaction hashunspent transaction output record for that transaction B 32 字节的 block hash: the block hash up to which the database represents the unspent transaction outputs

详情可见 这里

由于我们还没有交易,我们将只有blocks桶。另外,如上所述,我们将整个DB存储为单个文件,而不将块存储在单独的文件中。所以我们不需要任何与文件编号相关的内容。最终,我们会用到的键值对有:

l

这就是实现持久化机制所有需要了解的内容了。

Serialization

如前所述,在BoltDB中,值只能是[]byte类型,我们想存储Block数据库中的结构。我们会用的 encoding/gob 序列化结构。

让我们来实现 BlockSerialize 方法(为了简洁起见,此处略去了错误处理):

func (b *Block) Serialize() []byte {
	var result bytes.Buffer
	encoder := gob.NewEncoder(&result)

	err := encoder.Encode(b)

	return result.Bytes()
}

这个部分很简单:首先,我们声明一个存储序列化数据的缓冲区;然后我们初始化一个gob编码器和编码块;结果以字节数组的形式返回。

这个部分很简单:首先,我们声明一个存储序列化数据的缓冲区;然后我们初始化一个gob编码器和编码块;结果以字节数组的形式返回。

接下来,我们需要一个反序列化函数,它将接收一个字节数组作为输入并返回一个Block。这不是一种方法,而是一种独立的功能:

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

	decoder := gob.NewDecoder(bytes.NewReader(d))
	err := decoder.Decode(&block)

	return &block
}

这就是序列化!

Persistence

让我们从 NewBlockchain 函数开始。在之前的实现中, NewBlockchain 会创建一个新的 Blockchain 实例,并向其中加入创世块。而现在,我们希望它做的事情有:

  1. 打开一个数据库文件
  2. 检查是否存在区块链。
  3. 如果有区块链:创建一个新的 Blockchain 实例设置 Blockchain 实例的 tip 为数据库中存储的最后一个块的哈希
  4. 如果没有现有的区块链:创建创世块。存储到数据库将g创世块的哈希保存为最后一个块哈希。创建一个新的 Blockchain 实例,初始时 tip 指向创世块(tip 有尾部,尖端的意思,在这里 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中,使用数据库的操作在事务中运行。有两种类型的事务:只读和读写。在这里,我们打开一个读写事务(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密钥存储链的最后一个块哈希。

另外,注意创建 Blockchain 一个新的方式:

bc := Blockchain{tip, db}

我们不再存储其中的所有块,而是仅存储链的尖端。此外,我们存储数据库连接,因为我们想要打开它一次并在程序运行时保持打开状态。就这样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事务的另一种(只读)类型。在这里,我们从DB获取最后一个块哈希,以使用它来挖掘新的块哈希。

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中并更新l键,现在存储新块的哈希值。

完成!这不难,是吗?

Inspecting Blockchain

所有新块现在都保存在数据库中,因此我们可以重新打开区块链并向其添加新块。但是在实现之后,我们失去了一个很好的功能:我们不能再打印出区块链块了,因为我们不再将块存储在数组中。让我们解决这个缺陷吧!

BoltDB允许迭代桶中的所有键,但键按字节 排序 顺序存储,我们希望块按照它们在区块链中的顺序打印。另外,因为我们不想将所有块加载到内存中(我们的区块链数据库可能很大!或者只是假装它可以),我们将逐一阅读它们。为此,我们需要一个区块链迭代器:

type BlockchainIterator struct {
	currentHash []byte
	db          *bolt.DB
}

每当要对链中的块进行迭代时,我们就会创建一个迭代器,里面存储了当前迭代的块哈希( currentHash )和数据库的连接( db )。通过 db ,迭代器逻辑上被附属到一个区块链上(这里的区块链指的是存储了一个数据库连接的 Blockchain 实例),并且通过 Blockchain 方法进行创建:

func (bc *Blockchain) Iterator() *BlockchainIterator {
	bci := &BlockchainIterator{bc.tip, bc.db}

	return bci
}

请注意,迭代器最初指向区块链的顶端,因此将从上到下,从最新到最旧获得块。事实上, 选择一个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

到目前为止,我们的实现还没有提供任何与程序交互的接口:我们只是执行了 NewBlockchainbc.AddBlock 。是时候改善了!我们想要这些命令:

blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain

所有与命令行相关的操作都将由CLI struct处理:

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")

首先,我们创建两个子命令: addblockprintchain , 然后给 addblock 添加 -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

英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品经理全栈运营实战笔记

产品经理全栈运营实战笔记

林俊宇 / 化学工业出版社 / 49.8元

本书凝结作者多年的产品运营经验,读者会看到很多创业公司做运营的经验,书中列举了几十个互联网产品的运营案例去解析如何真正做好一个产品的冷启动到发展期再到平稳期。本书主要分为六篇:互联网运营的全面貌;我的运营生涯;后产品时代的运营之道;揭秘刷屏事件的背后运营;技能学习;深度思考。本书有很多关于产品运营的基础知识,会帮助你做好、做透。而且将理论和作者自己的案例以及其他人的运营案例结合起来,会让读者更容易......一起来看看 《产品经理全栈运营实战笔记》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具