在Go中构建区块链。 第3部分:持久性和CLI

介绍

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

数据库选择

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

我们需要哪个数据库? 其实,他们中的任何一个。 在[最初的比特币文件中] ,关于使用某个数据库没有任何说法,因此开发人员需要使用哪个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],但也可以使用JSONXMLProtocol Buffers等。 我们使用encoding/gob因为它很简单,并且是标准Go库的一部分。

数据库结构

在开始实施持久性逻辑之前,我们首先需要决定如何将数据存储在数据库中。 为此,我们将介绍比特币核心的做法。

简而言之,Bitcoin Core使用两个“存储桶”来存储数据:

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

另外,块在磁盘上作为单独的文件存储。 这是为了达到性能目的而完成的:读取单个块不需要将全部(或部分)全部加载到内存中。 我们不会执行这个。

blockskey -> value对是:

  1. 'b' + 32-byte block hash -> block index record
  2. 'f' + 4-byte file number -> file information record
  3. 'l' -> 4-byte file number: the last block file number used
  4. 'R' -> 1-byte boolean: whether we're in the process of reindexing
  5. 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
  6. 't' + 32-byte transaction hash -> transaction index record

chainstatekey -> value对是:

  1. 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
  2. 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

(详细解释可以在[这里]找到)

由于我们还没有交易,因此我们只会blocks存货桶。 另外,如上所述,我们将整个DB存储为单个文件,而不将块存储在单独的文件中。 所以我们不需要任何与文件编号有关的东西。 因此,这些是我们将使用的key -> value对:

  1. 32-byte block-hash -> Block structure (serialized)
  2. 'l' -> the hash of the last block in a chain

这就是我们开始实施持久性机制所需要知道的。

序列化

如前所述,BoltDB中的值只能是[]byte类型,并且我们希望将Block结构存储在数据库中。 我们将使用[编码/ gob]来序列化结构。

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

  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

(啤酒的声音可以打开)

结论
下次我们将实施地址,钱包和(可能)交易。 敬请期待!

Links

  1. Full source codes
  2. Bitcoin Core Data Storage
  3. boltdb
  4. encoding/gob
  5. flag
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容