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

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

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

资料

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

推荐阅读更多精彩内容

  • 1 伟大的开拓者-比特币 1) 比特币产生 2008年,中本聪(Satoshi Nakamoto)发表了一...
    金子_c38e阅读 7,334评论 0 18
  • 今天返校,第一次用番茄钟将打扫卫生整理床铺和箱柜等繁琐的事情做好,几个小时下来竟然觉得很充实,时间基本没有被浪费,...
    Daring_dd阅读 386评论 1 1
  • 换个其他类型的工作么? 还是不工作。 今天领导说起来其他部门领导在找他要人。不知道会要谁但是我觉得自己挺危险的,毕...
    lichangan阅读 148评论 0 0
  • 谭明明Tracy阅读 67评论 0 0
  • 相处是门技术活。 24岁即将25岁的自己,刚刚踏入社会,离开家庭,离开安全舒适的人际圈,被迫而不得不的去发生的一些...
    嘻嘻嗯嗯阅读 176评论 0 0