使用 Go 语言打造区块链(三): 编写持久层和客户端

in #cn7 years ago
  1. 介绍
  2. 数据库选择BoltDB
  3. 数据结构
  4. 序列化处理
  5. 持久层
  6. 查看并打印区块链
  7. 命令行界面

1. 介绍

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

2. 数据库选择: BoltDB

  1. 简洁
  2. 使用Go语言编写
  3. 不需要服务器即可运行
  4. 支持数据结构

译者注:Github上七千多个star足以见得,这是非常优秀的项目

BoltDB使用键值配对的方法

从BoltDB在Github上的README:

Bolt是一个纯粹Key/Value模型的程序。该项目的目标是为不需要完整数据库服务器(如Postgres或MySQL)的项目提供一个简单,快速,可靠的数据库。

BoltDB是一个Key/Value(键/值)存储,这意味着没有像SQL RDBMS(MySQL,PostgreSQL等)中的表,没有行,没有列。相反,数据作为键值对存储(如在Golang Maps中)。键值对存储在Buckets中,它们旨在对相似的对进行分组(这与RDBMS中的表类似)。因此,为了获得Value(值),需要知道该Value所在的桶和钥匙。

关于buckets的使用具体查看:https://github.com/boltdb/bolt#using-buckets

所有keys在同一个bucket中都是独一无二的

关于key-value的使用Bucket.Put(Key, Value)Bucket.Get()https://github.com/boltdb/bolt#using-keyvalue-pairs

BoltDB的一个重要事情是没有数据类型:键和值是字节数组。由于我们将存储Go结构(特别是Block),我们需要对它们进行序列化(Serialization),序列化即实现将Go Struct和字节数组(Byte array)直接的相互转换。Go语言自带的包encoding / gob,BoltDB也支持JSON,XML,协议缓冲区等序列化方式。

查看golang,APIhttps://golang.org/pkg/encoding/gob/,即可了解encoding gob协议

Encoder和Decoder

3. BoltDB数据库结构

在开始执行持久性逻辑之前,我们首先需要决定如何在数据库中存储数据。为此,我们将参考Bitcoin Core的做法。

简单来说,Bitcoin Core使用两个“bucket(桶)”来存储数据:

  1. blocks描述存储链中所有块的元数据。

  2. chainstate存储一个链的状态,这是所有当前未用的事务输出和一些元数据。

另外,块作为单独的文件存储在磁盘上。这是为了表现目的:读取单个块不需要读取所有区块。

在块中,Key 键 - > 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

4. 序列化Serialization

As said before, in BoltDB values can be only of []byte type, and we want to store Block structs in the DB. We’ll use encoding/gob to serialize the structs.

Let’s implement Serialize method of Block (errors processing is omitted for brevity):

序列化

如前所述,在BoltDB中,值values只能是bytes[]类型,我们要Blockstructs存储在数据库中。我们将使用encoding / gob来序列化structs。

实现Block的序列化方法:(go语言语句结尾不用分号)

//序列化 // 讲go语言的Block类型变量转换成[]byte
func (b *Block) Serialize() []byte {
    var result bytes.Buffer // 声明一个buffer,用于存储序列化数据
    encoder := gob.NewEncoder(&result) //利用gob编码,gob用于Encoder和Decoder之间的传输,一般使用RPC协议进行传输

    err := encoder.Encode(b) 

    return result.Bytes()
}

这一切很简单:首先,我们声明一个缓冲区,用于存储序列化数据;然后我们初始化一个gob编码器并对该块进行编码;结果作为字节数组返回。

//去序列化,从[]byte转换成go语言的Block类型
func DeserializeBlock(d []byte) *Block {
    var block Block

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

    return &block
}

5. 持久层处理

5.1. 给type Blockchain新增加一个member

type Blockchain struct {
    blocks []*Block
    db  *bolt.DB // 给type Blockchain中新增一个member
}

5.2. 修改NewBlockchain()function

我们之前的 NewBlockchain()

func NewBlockchain() *Blockchain {
  return &Blockchain{[]*Block{NewGenesisBlock()}}
}

新增逻辑将NewBlockchain存储在数据库中

func NewBlockchain() *Blockchain {
    var tip []byte
    db, err := bolt.Open(dbFile, 0600, nil)//打开BoltDB的一个文件,使用具体查看
  //https://github.com/boltdb/bolt#opening-a-database
  
  
    /*
    BoltDB的读写操作
    err = db.Update(func(tx *bolt.Tx) error {
      ...
      return nil
    }
    https://github.com/boltdb/bolt#read-write-transactions
    */
    err = db.Update(func(tx *bolt.Tx) error {  
        b := tx.Bucket([]byte(blocksBucket)) //创建以blocksBucket为名字的Bucket

        if b == nil {
            genesis := NewGenesisBlock()  //创建genesis block
            b, err := tx.CreateBucket([]byte(blocksBucket)) // 创建bucket,并将区块保存在bucket中
            err = b.Put(genesis.Hash, genesis.Serialize()) 
            err = b.Put([]byte("l"), genesis.Hash) //将key为l的value保存为整个区块链中的最后一个block的哈希值
            tip = genesis.Hash // assign tip to 
        } else {
            tip = b.Get([]byte("l")) // assign  
        }

        return nil
    })

    bc := Blockchain{tip, db}//我们通过存储区块中的tip,来储存区块。

    return &bc
}

/*
步骤小结:
1. 打开BoltDB文件
2. 检查是否存在blockchain
3. If there’s a blockchain:
   1. Create a new Blockchain instance.
   2. Set the tip of the Blockchain instance to the last block hash stored in the DB.
3. 如果不存在区块链
    1. 创建初始block(genesis block)
    2. 保存genesis block 在数据库中
    3. 将新创建的blockchain中最后block的哈希值设置成genesis block的哈希值
    4. 创建新的区块链实例并将这个实例中的tip其指向genesis block
4. 如果存在区块链
    1. 将区块链实例的tip 指向存储在DB中的最后一个block哈希值 
*/

db.Update

5.3. 增添Block

//没有数据库的增添区块函数
func (bc *Blockchain) AddBlock(data string) { 
    prevBlock := bc.blocks[len(bc.blocks)-1]
    newBlock := NewBlock(data, prevBlock.Hash)
    bc.blocks = append(bc.blocks, newBlock)
}
//数据库的增添区块函数
func (bc *Blockchain) AddBlock(data string) { //新block的类型data
    var lastHash []byte

  err := bc.db.View(func(tx *bolt.Tx) error { // Bolt read-only function 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)) //创建以blocksBucket为名字的Bucket
        err := b.Put(newBlock.Hash, newBlock.Serialize()) //讲新block的哈希作为Key,把序列化的结果当做Value
        err = b.Put([]byte("l"), newBlock.Hash) // 
        bc.tip = newBlock.Hash //仅存储tip,即末端区块链的末端值

        return nil
    })
}

6. 查看并打印区块链

所有新的块现在都保存在数据库中,并可以给区块链添加新的区块。但将区块链存储在数据库中之后,我们就很难打印出区块链的内容。

这一部分见

BoltDB allows to iterate over all the keys in a bucket, but the keys are stored in byte-sorted order, and we want blocks to be printed in the order they take in a blockchain. Also, because we don’t want to load all the blocks into memory (our blockchain DB could be huge!.. or let’s just pretend it could), we’ll read them one by one. For this purpose, we’ll need a blockchain iterator:

BoltDB允许遍历桶中的所有密钥,但密钥按字节排序的顺序存储

打印要求
  1. 按照它们在区块链中的原有顺序打印出来。
  2. 不能将所有的块加载到内存中(但数据库容量巨大,但内存容量有限),我们将逐个读取它们。
创建迭代器
type BlockchainIterator struct {
    currentHash []byte
    db          *bolt.DB
}

Notice that an iterator initially points at the tip of a blockchain, thus blocks will be obtained from top to bottom, from newest to oldest. In fact, choosing a tip means “voting” for a blockchain. A blockchain can have multiple branches, and it’s the longest of them that’s considered main. After getting a tip (it can be any block in the blockchain) we can reconstruct the whole blockchain and find its length and the work required to build it. This fact also means that a tip is a kind of an identifier of a blockchain.

请注意,迭代器最初指向块链的末端,因此块将从上到下从最新产生的区块向过去的区块遍历。事实上,选择tip意味向一个块“投票”。一个块链可以有多个分支,最长的那个链被人为是最重要。

得到一个tip,我们可以重建整个块链,并了解该块链的长度和构建它所需的工作。这个事实也意味着tip是一种块链的标识符。

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) // 从数据库中的[]byte形式传唤成可读的

        return nil
    })

    i.currentHash = block.PrevBlockHash //从最新的区块向老区块遍历

    return block
}

7. 命令行界面

执行刚刚创立的NewBlockchainbc.AddBlockmain函数

type CLI struct {
    bc *Blockchain
}

客户端界面的介入函数

func (cli *CLI) Run() {
    cli.validateArgs()
    //利用package flag,讲error打印成string的形式
    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)
    }
    //是否已经转换成string
    if addBlockCmd.Parsed() {  // https://golang.org/pkg/flag/#Parsed
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

    if printChainCmd.Parsed() {
        cli.printChain()
    }
}

什么是parse

z检查是否成功转换成string

添加命令行增加block和打印的函数

//再添加
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
        }
    }
}

给main函数增添数据库命令行功能

func main() {
    bc := NewBlockchain()
    defer bc.db.Close()

    cli := CLI{bc}
    cli.Run()
}

defer:推迟

defer statement会在围绕这个这个statement的其他statement执行之后执行

package main

import "fmt"

func main() {
    fmt.Println("hello")
    defer fmt.Println("world")
    fmt.Println("go")
    fmt.Println("lang")
}
/* 
hello
go
lang
world
*/

下面

接下来文章会包含地址,钱包,交易等。Excited !

Links

  1. Full source codes
  2. Bitcoin Core Data Storage
  3. boltdb
  4. encoding/gob
  5. flag
Sort:  

Hi! I am a robot. I just upvoted you! I found similar content that readers might be interested in:
https://jeiwan.cc/posts/building-blockchain-in-go-part-3/