IT技术区块链研习社区块链研究

Go实现区块链(三)---存储与命令

2018-03-16  本文已影响124人  even_366

1.前言

到目前为止我们了解区块链的数据结构以及简易版的挖矿(pow共识机制)。接下来我们将一起了解区块链的存储,注意:区块链本质上一款分布式数据库,这里不实现分布式,我们这先了解区块链存储部分。

2.知识准备

知识点 学习网页 特性
比特币数据库 leveldb 1.key和value都是任意长度的字节数组;2.entry(即一条K-V记录)默认是按照key的字典顺序存储的,当然开发者也可以重载这个排序函数;3.提供的基本操作接口:Put()、Delete()、Get()、Batch();4.支持批量操作以原子操作进行;5.可以创建数据全景的snapshot(快照),并允许在快照中查找数据;6.可以通过前向(或后向)迭代器遍历数据(迭代器会隐含的创建一个snapshot);7.自动使用Snappy压缩数据;8、可移植性;
BoltDB数据库 boltDB 1.它简单而简约;2.它在Go中实现;3.它不需要运行服务器;4.它允许构建我们想要的数据结构。
go中序列化 由于数据库是字节码的方式存储这里我们需要序列化对象,采用encoding/gob包

这里我们将会用道boltdb数据库来存储我们的数据。

3.数据结构

我们看看比特币数据库是怎么存储的。

简单理解,比特币使用了两个"buckets"(桶)来存储数据:

"在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO"

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

在blocks这个桶中,存储的是键值对:

#块索引记
'b' + 32-byte block hash -> block index record
#文件信息记录
'f' + 4-byte file number -> file information record
#使用的最后一个块文件编号
'l' -> 4-byte file number: the last block file number used
#是否处于重建索引的进程当中
'R' -> 1-byte boolean: whether we're in the process of reindexing
#各种可以打开或关闭的flag标志
'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
#交易索引记录
't' + 32-byte transaction hash -> transaction index record

在 chainstate 这个桶中,存储的键值对:

#某笔交易的UTXO记录
'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
#数据库表示未使用的事务输出的块散列
'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

比特币存储详情

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

#区块数据与区块hash的键值对
32-byte block-hash -> Block structure (serialized)
#链中最后一个块的散列
'l' -> the hash of the last block in a chain

4.序列化

由于这里Key与Value采用[]byte的形式存储,所以我们需要序列化,采用Go提供的encoding/gob来实现序列化与反序列化。

//序列化Block
func (b *Block) Serialize() []byte  {
    var result bytes.Buffer
    encoder := gob.NewEncoder(&result)
    err := encoder.Encode(b)
    if err != nil {
        log.Panic(err)
    }
    return result.Bytes()
}
//反序列化
func DeserializeBlock(d []byte) *Block {
    var block Block

    decoder := gob.NewDecoder(bytes.NewReader(d))
    err := decoder.Decode(&block)
    if err != nil {
        log.Panic(err)
    }

    return &block
}

5.存储区块数据流程图

存储区块数据流程

代码实现:

// 创建一个新的区块链和创世块
func NewBlockchain() *Blockchain {
    var tip []byte
    //打开数据库
    db, err := bolt.Open(dbFile, 0600, nil)
    if err != nil {
        log.Panic(err)
    }
    
    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        
        if b == nil {
            fmt.Println("No existing blockchain found. Creating a new one...")
            genesis := NewGenesisBlock()

            b, err := tx.CreateBucket([]byte(blocksBucket))
            if err != nil {
                log.Panic(err)
            }
            err = b.Put(genesis.Hash, genesis.Serialize())
            if err != nil {
                log.Panic(err)
            }
            err = b.Put([]byte("l"), genesis.Hash)
            if err != nil {
                log.Panic(err)
            }
            tip = genesis.Hash
        } else {
            tip = b.Get([]byte("l"))
        }
        return nil
    })
    if err != nil {
        log.Panic(err)
    }
    bc := Blockchain{tip, db}
    return &bc
}

这是打开BoltDB文件的标准方式。 注意,如果没有这样的文件,它不会返回错误。

添加区块方法AddBlock:现在我们添加区块并不会向数组添加元素那么简单,我们将block存储在DB中:

//添加区块
func (bc *Blockchain) AddBlock(data string)  {
    var lastHash []byte //最后一个区块hash
    //查询数据库中最后一块的hash
    err :=bc.db.View(func(tx *bolt.Tx) error {
        b :=tx.Bucket([]byte(blocksBucket))
        lastHash=b.Get([]byte("1"))//最新的一块hash的key我们知道为"l"
        return nil
    })

    if err!=nil{
        log.Panic(err)
    }
    //利最后的一块hash,挖掘一块新的区块出来
    newBlock :=NewBlock(data,lastHash)
    //在挖掘新块之后,我们将其序列化表示保存到数据块中并更新"l",该密钥现在存储新块的哈希。
    err=bc.db.Update(func(tx *bolt.Tx) error {
        b :=tx.Bucket([]byte(blocksBucket))
        err :=b.Put(newBlock.Hash,newBlock.Serialize())
        if err!=nil{
            log.Panic(err)
        }
        err = b.Put([]byte("l"), newBlock.Hash)
        if err != nil {
            log.Panic(err)
        }
        bc.tip = newBlock.Hash
        return nil
    })
}

好了存储区块桶实现了,接下来我们想实现查看区块链的区块数据。

6.检索区块链

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

// 区块链迭代器用于迭代区块
type BlockchainIterator struct {
    currentHash []byte //当前的hash
    db          *bolt.DB //数据库
}

每次我们想要遍历区块链中的块时,都会创建一个迭代器,它将存储当前迭代的块散列和到数据库的连接。由于后者,迭代器在逻辑上被附加到区块链(它是一个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
    })
    if err != nil {
        log.Panic(err)
    }
       //将前一个区块
    i.currentHash = block.PrevBlockHash

    return block
}

7.CLI

目前为止我们并没有提供任何接口与程序交互,都是通过main函数里面来调用方法,我们想通过命令的方式来执行这些方法。封装一个cli:

type CLI struct {
    bc *block.Blockchain //区块链
}

提供一个接口供main调用接口。

//启动接口函数
func Start(bc *block.Blockchain)interface{}  {
    cl := CLI{bc}
    cl.run()//执行命令方法
    return  nil
}
//打印用法
func (cli *CLI) printUsage()  {
    fmt.Println("Usage:")
    fmt.Println("  addblock -data BLOCK_DATA - add a block to the blockchain")
    fmt.Println("  printchain - print all the blocks of the blockchain")
}
//校验参数
func (cli *CLI) validateArgs() {
    if len(os.Args) < 2 {
        cli.printUsage()
        os.Exit(1)
    }
}
//添加区块数据
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)

        fmt.Printf("PoW: %s\n", strconv.FormatBool(block.Validate()))
        fmt.Println()
        //创世块是没有前一个区块的,所以PrevBlockHash的值是没有的
        if len(block.PrevBlockHash) == 0 {
            break
        }
    }
}

// 执行命令方法
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:])
        if err != nil {
            log.Panic(err)
        }
    case "printchain":
        err := printChainCmd.Parse(os.Args[2:])
        if err != nil {
            log.Panic(err)
        }
    default:
        cli.printUsage()
        os.Exit(1)
    }

    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            os.Exit(1)
        }
        cli.addBlock(*addBlockData)
    }

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

修改main

func main() {
    bc := block.NewBlockchain()
    defer block.Close(bc)
    cli.Start(bc)
}

构建go项目命令:

C:\go-worke\src\github.com\study-bitcion-go>go build github.com/study-bitcion-go

命令方式添加一个数据:

C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go addblock -data "even send tom 1.000000BTC"
Mining the block containing "even send tom 1.000000BTC"
 Dig into mine  0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635

Success!

迭代区块链数据:

C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go printchain
Prev. hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
Data: even send tom 1.000000BTC
Hash: 0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635
PoW: true

Prev. hash:
Data: Genesis Block
Hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
PoW: true

本章实现了数据持久化存储,命令方式启动。后续我们将实现钱包、交易、网络等。

资料

上一篇下一篇

猜你喜欢

热点阅读