内容简介:到目前为止,我们已经建立了一个带有工作量证明系统的区块链,这使得挖矿成为可能。我们的实现越来越接近功能齐全的区块链,但它仍然缺乏一些重要的功能。今天将开始在数据库中存储区块链,之后我们将创建一个简单的命令行界面来执行区块链操作。从本质上讲,区块链是一个分布式数据库。我们暂时将省略“分布式”部分,并专注于“数据库”部分。目前,我们的实现中没有数据库;相反,我们每次运行程序时都会创建块并将它们存储在内存中。我们无法重用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。我们需要哪个数据库?实际上,任何一
Introduction
到目前为止,我们已经建立了一个带有工作量证明系统的区块链,这使得挖矿成为可能。我们的实现越来越接近功能齐全的区块链,但它仍然缺乏一些重要的功能。今天将开始在数据库中存储区块链,之后我们将创建一个简单的命令行界面来执行区块链操作。从本质上讲,区块链是一个分布式数据库。我们暂时将省略“分布式”部分,并专注于“数据库”部分。
Database Choice
目前,我们的实现中没有数据库;相反,我们每次运行程序时都会创建块并将它们存储在内存中。我们无法重用区块链,我们无法与其他人共享,因此我们需要将其存储在磁盘上。
我们需要哪个数据库?实际上,任何一个数据库都可以。在 the original Bitcoin paper 没有任何关于使用某个数据库的说法,因此由开发人员决定使用哪个数据库。 Bitcoin Core ,最初由中本聪发布,目前是比特币的参考实现,使用 LevelDB 。而我们将要使用的是....
BoltDB
因为:
- 它足够简单。
- 它使用 Go 实现。
- 它不需要运行服务器。
- 它允许构建我们想要的数据结构。
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
在开始实现持久性逻辑之前,我们首先需要决定如何在数据库中存储数据。为此,我们将参考比特币核心的方式。
简单来说,比特币核心使用两个“桶”来存储数据:
- blocks存储描述链中所有块的元数据。
- 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
在 chainstate , key -> 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 序列化结构。
让我们来实现 Block
的 Serialize
方法(为了简洁起见,此处略去了错误处理):
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
实例,并向其中加入创世块。而现在,我们希望它做的事情有:
- 打开一个数据库文件
- 检查是否存在区块链。
-
如果有区块链:创建一个新的
Blockchain
实例设置Blockchain
实例的 tip 为数据库中存储的最后一个块的哈希 -
如果没有现有的区块链:创建创世块。存储到数据库将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
到目前为止,我们的实现还没有提供任何与程序交互的接口:我们只是执行了 NewBlockchain
和 bc.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")
首先,我们创建两个子命令: addblock
和 printchain
, 然后给 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/
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
微信小程序(开发入门及案例详解)
李骏、边思 / 机械工业出版社 / 2017-3-1 / 59.0
本书可分为3部分,第一部分作为基础章节,介绍了第一个小程序的搭建流程,让大家能快速上手;同时对小程序框架原理进行了详细介绍,为后面学习组件、API打下基础。 第二部分对小程序组件、API进行介绍,对组件、API的使用、注意事项进行详细讲解,并给出示例代码。 最后一部分精选5个由浅入深的案例,对小程序研发进行实战讲解,涵盖了实际项目中可能涉及的技术方案和使用方法,具备很强的实战意义。 ......一起来看看 《微信小程序(开发入门及案例详解)》 这本书的介绍吧!