美文网首页IT技术区块链研习社区块链研究
Go实现区块链(三)---存储与命令

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

作者: even_366 | 来源:发表于2018-03-16 17:04 被阅读124次

    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"(桶)来存储数据:

    • blocks 描述链上所有区块的元数据。
    • chainstate 存储区块链的状态,指的是当前所有的UTXO(未花费交易输出)以及一些元数据。

    "在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的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
    
    

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

    资料

    相关文章

      网友评论

        本文标题:Go实现区块链(三)---存储与命令

        本文链接:https://www.haomeiwen.com/subject/ibccqftx.html