Go實(shí)現(xiàn)區(qū)塊鏈(三)---存儲與命令

1.前言

到目前為止我們了解區(qū)塊鏈的數(shù)據(jù)結(jié)構(gòu)以及簡易版的挖礦(pow共識機(jī)制)。接下來我們將一起了解區(qū)塊鏈的存儲憎蛤,注意:區(qū)塊鏈本質(zhì)上一款分布式數(shù)據(jù)庫龄毡,這里不實(shí)現(xiàn)分布式,我們這先了解區(qū)塊鏈存儲部分朵逝。

2.知識準(zhǔn)備

知識點(diǎn) 學(xué)習(xí)網(wǎng)頁 特性
比特幣數(shù)據(jù)庫 leveldb 1.key和value都是任意長度的字節(jié)數(shù)組;2.entry(即一條K-V記錄)默認(rèn)是按照key的字典順序存儲的乡范,當(dāng)然開發(fā)者也可以重載這個排序函數(shù)配名;3.提供的基本操作接口:Put()、Delete()晋辆、Get()渠脉、Batch();4.支持批量操作以原子操作進(jìn)行瓶佳;5.可以創(chuàng)建數(shù)據(jù)全景的snapshot(快照)芋膘,并允許在快照中查找數(shù)據(jù);6.可以通過前向(或后向)迭代器遍歷數(shù)據(jù)(迭代器會隱含的創(chuàng)建一個snapshot)霸饲;7.自動使用Snappy壓縮數(shù)據(jù)为朋;8、可移植性厚脉;
BoltDB數(shù)據(jù)庫 boltDB 1.它簡單而簡約;2.它在Go中實(shí)現(xiàn);3.它不需要運(yùn)行服務(wù)器;4.它允許構(gòu)建我們想要的數(shù)據(jù)結(jié)構(gòu)习寸。
go中序列化 由于數(shù)據(jù)庫是字節(jié)碼的方式存儲這里我們需要序列化對象,采用encoding/gob包

這里我們將會用道boltdb數(shù)據(jù)庫來存儲我們的數(shù)據(jù)傻工。

3.數(shù)據(jù)結(jié)構(gòu)

我們看看比特幣數(shù)據(jù)庫是怎么存儲的霞溪。

簡單理解,比特幣使用了兩個"buckets"(桶)來存儲數(shù)據(jù):

  • blocks 描述鏈上所有區(qū)塊的元數(shù)據(jù)孵滞。
  • chainstate 存儲區(qū)塊鏈的狀態(tài),指的是當(dāng)前所有的UTXO(未花費(fèi)交易輸出)以及一些元數(shù)據(jù)鸯匹。

"在比特幣的世界里既沒有賬戶坊饶,也沒有余額,只有分散到區(qū)塊鏈里的UTXO"

另外殴蓬,塊在磁盤上作為單獨(dú)的文件存儲匿级。 這是為了達(dá)到性能目的而完成的:讀取單個塊不需要將全部(或部分)全部加載到內(nèi)存中。 我們不會執(zhí)行這個染厅。

在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
#是否處于重建索引的進(jìn)程當(dāng)中
'R' -> 1-byte boolean: whether we're in the process of reindexing
#各種可以打開或關(guān)閉的flag標(biāo)志
'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
#數(shù)據(jù)庫表示未使用的事務(wù)輸出的塊散列
'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

比特幣存儲詳情

由于我們還沒有交易,因此我們只會封裝bucket糟秘。 另外简逮,如上所述,我們將整個DB存儲為單個文件尿赚,而不將塊存儲在單獨(dú)的文件中散庶。 所以我們不需要任何與文件編號有關(guān)的東西。 因此凌净,這些是我們將使用的鍵 - >值對:

#區(qū)塊數(shù)據(jù)與區(qū)塊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來實(shí)現(xiàn)序列化與反序列化悲龟。

//序列化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.存儲區(qū)塊數(shù)據(jù)流程圖

存儲區(qū)塊數(shù)據(jù)流程

代碼實(shí)現(xiàn):

// 創(chuàng)建一個新的區(qū)塊鏈和創(chuàng)世塊
func NewBlockchain() *Blockchain {
    var tip []byte
    //打開數(shù)據(jù)庫
    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文件的標(biāo)準(zhǔn)方式。 注意冰寻,如果沒有這樣的文件须教,它不會返回錯誤。

添加區(qū)塊方法AddBlock:現(xiàn)在我們添加區(qū)塊并不會向數(shù)組添加元素那么簡單斩芭,我們將block存儲在DB中:

//添加區(qū)塊
func (bc *Blockchain) AddBlock(data string)  {
    var lastHash []byte //最后一個區(qū)塊hash
    //查詢數(shù)據(jù)庫中最后一塊的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轻腺,挖掘一塊新的區(qū)塊出來
    newBlock :=NewBlock(data,lastHash)
    //在挖掘新塊之后,我們將其序列化表示保存到數(shù)據(jù)塊中并更新"l"划乖,該密鑰現(xiàn)在存儲新塊的哈希贬养。
    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
    })
}

好了存儲區(qū)塊桶實(shí)現(xiàn)了,接下來我們想實(shí)現(xiàn)查看區(qū)塊鏈的區(qū)塊數(shù)據(jù)琴庵。

6.檢索區(qū)塊鏈

BoltDB允許迭代桶中的所有鍵误算,但鍵以字節(jié)排序的順序存儲,我們希望塊按照它們在區(qū)塊鏈中的順序進(jìn)行打印迷殿。另外儿礼,因?yàn)槲覀儾幌雽⑺袎K加載到內(nèi)存中(我們的區(qū)塊鏈數(shù)據(jù)庫可能很大,或者我們假裝它可以)庆寺,我們將逐個讀取它們蚊夫。為此,我們需要一個區(qū)塊鏈迭代器:

// 區(qū)塊鏈迭代器用于迭代區(qū)塊
type BlockchainIterator struct {
    currentHash []byte //當(dāng)前的hash
    db          *bolt.DB //數(shù)據(jù)庫
}

每次我們想要遍歷區(qū)塊鏈中的塊時止邮,都會創(chuàng)建一個迭代器这橙,它將存儲當(dāng)前迭代的塊散列和到數(shù)據(jù)庫的連接。由于后者导披,迭代器在邏輯上被附加到區(qū)塊鏈(它是一個Blockchain存儲數(shù)據(jù)庫連接的實(shí)例)屈扎,因此在一個Blockchain方法中創(chuàng)建:

//迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
    bci := &BlockchainIterator{bc.tip, bc.db}
    return bci
}

BlockchainIterator 只會做一件事:它會從區(qū)塊鏈返回下一個區(qū)塊。

// 迭代下一區(qū)塊
func (i *BlockchainIterator) Next() *Block {
    var block *Block

    err := i.db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(blocksBucket))
        //查詢區(qū)塊
        encodedBlock := b.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)

        return nil
    })
    if err != nil {
        log.Panic(err)
    }
       //將前一個區(qū)塊
    i.currentHash = block.PrevBlockHash

    return block
}

7.CLI

目前為止我們并沒有提供任何接口與程序交互撩匕,都是通過main函數(shù)里面來調(diào)用方法鹰晨,我們想通過命令的方式來執(zhí)行這些方法。封裝一個cli:

type CLI struct {
    bc *block.Blockchain //區(qū)塊鏈
}

提供一個接口供main調(diào)用接口止毕。

//啟動接口函數(shù)
func Start(bc *block.Blockchain)interface{}  {
    cl := CLI{bc}
    cl.run()//執(zhí)行命令方法
    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")
}
//校驗(yàn)參數(shù)
func (cli *CLI) validateArgs() {
    if len(os.Args) < 2 {
        cli.printUsage()
        os.Exit(1)
    }
}
//添加區(qū)塊數(shù)據(jù)
func (cli *CLI) addBlock(data string) {
    cli.bc.AddBlock(data)
    fmt.Println("Success!")
}
//打印區(qū)塊鏈上所有區(qū)塊數(shù)據(jù)
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()
        //創(chuàng)世塊是沒有前一個區(qū)塊的模蜡,所以PrevBlockHash的值是沒有的
        if len(block.PrevBlockHash) == 0 {
            break
        }
    }
}

// 執(zhí)行命令方法
func (cli *CLI) run() {
    cli.validateArgs()//校驗(yàn)參數(shù)
    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)
}

構(gòu)建go項(xiàng)目命令:

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

命令方式添加一個數(shù)據(jù):

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!

迭代區(qū)塊鏈數(shù)據(jù):

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

本章實(shí)現(xiàn)了數(shù)據(jù)持久化存儲,命令方式啟動扁凛。后續(xù)我們將實(shí)現(xiàn)錢包忍疾、交易、網(wǎng)絡(luò)等谨朝。

資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卤妒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子字币,更是在濱河造成了極大的恐慌则披,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洗出,死亡現(xiàn)場離奇詭異士复,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)翩活,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進(jìn)店門阱洪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人菠镇,你說我怎么就攤上這事澄峰。” “怎么了辟犀?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵俏竞,是天一觀的道長。 經(jīng)常有香客問我堂竟,道長魂毁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任出嘹,我火速辦了婚禮席楚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘税稼。我一直安慰自己烦秩,他們只是感情好垮斯,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著只祠,像睡著了一般兜蠕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抛寝,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天熊杨,我揣著相機(jī)與錄音,去河邊找鬼盗舰。 笑死晶府,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钻趋。 我是一名探鬼主播川陆,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蛮位!你這毒婦竟也來了书劝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤土至,失蹤者是張志新(化名)和其女友劉穎购对,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陶因,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骡苞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了楷扬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片解幽。...
    茶點(diǎn)故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖烘苹,靈堂內(nèi)的尸體忽然破棺而出躲株,到底是詐尸還是另有隱情,我是刑警寧澤镣衡,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布霜定,位于F島的核電站,受9級特大地震影響廊鸥,放射性物質(zhì)發(fā)生泄漏望浩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一惰说、第九天 我趴在偏房一處隱蔽的房頂上張望磨德。 院中可真熱鬧,春花似錦、人聲如沸典挑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽您觉。三九已至拙寡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顾犹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工褒墨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炫刷,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓郁妈,卻偏偏與公主長得像浑玛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子噩咪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1 偉大的開拓者-比特幣 1) 比特幣產(chǎn)生 2008年顾彰,中本聰(Satoshi Nakamoto)發(fā)表了一...
    金子_c38e閱讀 7,334評論 0 18
  • 今天返校,第一次用番茄鐘將打掃衛(wèi)生整理床鋪和箱柜等繁瑣的事情做好胃碾,幾個小時下來竟然覺得很充實(shí)涨享,時間基本沒有被浪費(fèi),...
    Daring_dd閱讀 386評論 1 1
  • 換個其他類型的工作么仆百? 還是不工作厕隧。 今天領(lǐng)導(dǎo)說起來其他部門領(lǐng)導(dǎo)在找他要人。不知道會要誰但是我覺得自己挺危險的俄周,畢...
    lichangan閱讀 148評論 0 0
  • 譚明明Tracy閱讀 67評論 0 0
  • 相處是門技術(shù)活吁讨。 24歲即將25歲的自己,剛剛踏入社會峦朗,離開家庭建丧,離開安全舒適的人際圈,被迫而不得不的去發(fā)生的一些...
    嘻嘻嗯嗯閱讀 176評論 0 0