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 为:

keyvalueb + 32 字节的 block hashblock index recordf + 4 字节的 file numberfile information recordl + 4 字节的 file numberthe last block file number usedR + 1 字节的 boolean是否正在 reindexF + 1 字节的 flag name length + flag name string1 byte boolean: various flags that can be on or offt + 32 字节的 transaction hashtransaction index record

chainstatekey -> value 为:

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

详情可见 这里

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

  1. 32 字节的 block-hash -> block 结构
  2. l -> 链中最后一个块的 hash

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

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/