上一節學習了基於go語言的資料庫boltDB的基本使用,這一節用boltDB實現區塊鏈的資料持久化。
儲存方式
區塊鏈的資料主要集中在各個區塊上,所以區塊鏈的資料持久化即可轉化為對每一個區塊的儲存。boltDB是KV儲存方式,因此這裡我們可以以區塊的雜湊值為Key,區塊為Value。
此外,我們還需要儲存最新區塊的雜湊值。這樣,就可以找到最新的區塊,然後按照區Block Storage的上個區塊雜湊值找到上個區塊,以此類推便可以找到區塊鏈上所有的區塊。
區塊序列化
我們知道,boltDB儲存的索引值對的資料類型都是位元組數組。所以在儲存區塊前需要對區塊進行序列化,當然讀取區塊的時候就需要做還原序列化處理。
沒什麼痛點,都是藉助系統方法實現。廢話少說上代碼。
序列化
//區塊序列化func (block *Block) Serialize() []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) err := encoder.Encode(block) if err != nil{ log.Panic(err) } return result.Bytes()}
還原序列化
//區塊還原序列化func DeSerializeBlock(blockBytes []byte) *Block { var block *Block dencoder := gob.NewDecoder(bytes.NewReader(blockBytes)) err := dencoder.Decode(&block) if err != nil{ log.Panic(err) } return block}
區塊鏈類
區塊鏈結構
之前定義的區塊鏈結構是這樣的:
type Blockchain struct { //有序區塊的數組 Blocks [] *Block}
但是這樣的結構,每次運行程式區塊數組都是從零開始建立,並不能實現區塊鏈的資料持久化。這裡的數組屬性要改為boltDB類型的區塊資料庫,同時還必須有一個儲存當前區塊鏈最新區塊雜湊的屬性。
type Blockchain struct { //最新區塊的Hash Tip []byte //儲存區塊的資料庫 DB *bolt.DB}
相關資料庫常量
//相關資料庫屬性const dbName = "chaorsBlockchain.db"const blockTableName = "chaorsBlocks"const newestBlockKey = "chNewestBlockKey"
建立區塊鏈
//1.建立帶有創世區塊的區塊鏈func CreateBlockchainWithGensisBlock() *Blockchain { var blockchain *Blockchain //判斷資料庫是否存在 if IsDBExists(dbName) { db, err := bolt.Open(dbName, 0600, nil) if err != nil { log.Fatal(err) } err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if b != nil { hash := b.Get([]byte(newestBlockKey)) blockchain = &Blockchain{hash, db} //fmt.Printf("%x", hash) } return nil }) if err != nil { log.Panic(err) } //blockchain.Printchain() //os.Exit(1) return blockchain } //建立並開啟資料庫 db, err := bolt.Open(dbName, 0600, nil) if err != nil { log.Fatal(err) } err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) //blockTableName不存在再去建立表 if b == nil { b, err = tx.CreateBucket([]byte(blockTableName)) if err != nil { log.Panic(err) } } if b != nil { //創世區塊 gensisBlock := CreateGenesisBlock("Gensis Block...") //存入資料庫 err := b.Put(gensisBlock.Hash, gensisBlock.Serialize()) if err != nil { log.Panic(err) } //儲存最新區塊hash err = b.Put([]byte(newestBlockKey), gensisBlock.Hash) if err != nil { log.Panic(err) } blockchain = &Blockchain{gensisBlock.Hash, db} } return nil }) //更新資料庫失敗 if err != nil { log.Fatal(err) } return blockchain}
新增區塊
前面我們寫的這個方法為:
func (blc *Blockchain) AddBlockToBlockchain(data string, height int64, prevHash []byte) {
仔細看發現,參數好多顯得巨繁瑣。那是否有些參數是沒必要傳遞的呢?
我們既然用資料庫實現了區塊鏈的資料持久化,這裡的高度height可以根據上個區塊高度自增,prevHash也可以從資料庫中取出上個區塊而得到。因此,從今天開始,該方法省去這兩個參數。
//2.新增一個區塊到區塊鏈func (blc *Blockchain) AddBlockToBlockchain(data string) { err := blc.DB.Update(func(tx *bolt.Tx) error { //1.取表 b := tx.Bucket([]byte(blockTableName)) if b != nil { //2.height,prevHash都可以從資料庫中取到 當前最新區塊即添加後的上一個區塊 blockBytes := b.Get(blc.Tip) block := DeSerializeBlock(blockBytes) //3.建立新區快 newBlock := NewBlock(data, block.Height+1, block.Hash) //4.區塊序列化入庫 err := b.Put(newBlock.Hash, newBlock.Serialize()) if err != nil { log.Fatal(err) } //5.更新資料庫裡最新區塊 err = b.Put([]byte(newestBlockKey), newBlock.Hash) if err != nil { log.Fatal(err) } //6.更新區塊鏈最新區塊 blc.Tip = newBlock.Hash } return nil }) if err != nil { log.Fatal(err) }}
區塊鏈遍曆
//3.遍曆輸出所有區塊資訊 --> 以後一般使用最佳化後的迭代器方法(見3.X)func (blc *Blockchain) Printchain() { var block *Block //當前遍曆的區塊hash var curHash []byte = blc.Tip for { err := blc.DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if b != nil { blockBytes := b.Get(curHash) block = DeSerializeBlock(blockBytes) /**時間戳記格式化 Format裡的年份必須是固定的!!! 這個好像是go誕生的時間 time.Unix(block.Timestamp, 0).Format("2006-01-02 15:04:05") "2006-01-02 15:04:05"格式固定,改變其他也可能會出錯 */ fmt.Printf("\n#####\nHeight:%d\nPrevHash:%x\nHash:%x\nData:%s\nTime:%s\nNonce:%d\n#####\n", block.Height, block.PrevBlockHash, block.Hash, block.Data, time.Unix(block.Timestamp, 0).Format("2006-01-02 15:04:05"), block.Nonce) } return nil }) if err != nil { log.Fatal(err) } var hashInt big.Int hashInt.SetBytes(block.PrevBlockHash) //遍曆到創世區塊,跳出迴圈 創世區塊雜湊為0 if big.NewInt(0).Cmp(&hashInt) == 0 { break } curHash = block.PrevBlockHash }}
注意:
time.Unix(block.Timestamp, 0).Format("2006-01-02 15:04:05") goLang這裡真是奇葩啊……時間戳記格式化只能寫"2006-01-02 15:04:05",一個數丟不能寫錯,不然你會”被穿越“的!!!據說這個日期是go語言的誕生日期,還真是傲嬌啊,生怕大家不知道嗎???
判斷區塊鏈資料庫是否存在
//判斷資料庫是否存在func IsDBExists(dbName string) bool { //if _, err := os.Stat(dbName); os.IsNotExist(err) { // // return false //} _, err := os.Stat(dbName) if err == nil { return true } if os.IsNotExist(err) { return false } return true}
區塊鏈迭代器
對區塊鏈區塊的遍曆上面已經實現,但是還可以最佳化。我們不難發現區塊鏈的區塊遍曆類似於單向鏈表的遍曆,那麼我們能不能製造一個像鏈表的Next屬性似的迭代器,只要通過不斷地訪問Next就能遍曆所有的區塊?
話都說到這份上了,答案當然是肯當的。
BlockchainIterator
//區塊鏈迭代器type BlockchainIterator struct { //當前遍曆hash CurrHash []byte //區塊鏈資料庫 DB *bolt.DB}
Next迭代方法
func (blcIterator *BlockchainIterator) Next() *Block { var block *Block //資料庫查詢 err := blcIterator.DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blockTableName)) if b != nil { //擷取當前迭代器對應的區塊 currBlockBytes := b.Get(blcIterator.CurrHash) block = DeSerializeBlock(currBlockBytes) //更新迭代器 blcIterator.CurrHash = block.PrevBlockHash } return nil }) if err != nil { log.Fatal(err) } return block}
怎麼用?
1.在Blockchain類新增一個產生當前區塊鏈的迭代器的方法
//產生當前區塊鏈迭代器的方法func (blc *Blockchain) Iterator() *BlockchainIterator { return &BlockchainIterator{blc.Tip, blc.DB}}
2.修改之前的Printchain方法
//3.X 最佳化區塊鏈遍曆方法func (blc *Blockchain) Printchain() { //迭代器 blcIterator := blc.Iterator() for { block := blcIterator.Next() fmt.Printf("\n#####\nHeight:%d\nPrevHash:%x\nHash:%x\nData:%s\nTime:%s\nNonce:%d\n#####\n", block.Height, block.PrevBlockHash, block.Hash, block.Data, time.Unix(block.Timestamp, 0).Format("2006-01-02 15:04:05"),block.Nonce) var hashInt big.Int hashInt.SetBytes(block.PrevBlockHash) if big.NewInt(0).Cmp(&hashInt) == 0 { break } }}
是不是發現遍曆區塊的代碼相對簡潔了,這裡把資料庫訪問和區塊迭代的代碼分離到了BlockchainIterator裡實現,也符合程式設計的單一職責原則。
main函數測試
package mainimport ( "chaors.com/LearnGo/publicChaorsChain/part4-DataPersistence-Prototype/BLC")func main() { blockchain := BLC.CreateBlockchainWithGensisBlock() defer blockchain.DB.Close() //添加一個新區快 blockchain.AddBlockToBlockchain("first Block") blockchain.AddBlockToBlockchain("second Block") blockchain.AddBlockToBlockchain("third Block") blockchain.Printchain()}
1.首次運行(這時不存在資料庫)
mainTest_1
2.注釋掉三句AddBlockToBlockchain代碼,再次運行
mainTest_2
這次我們並沒有添加區塊,所以列印區沒有挖礦的過程。但是列印的區塊是上次AddBlockToBlockchain添加的,說明區Block Storage成功了。
2.修改AddBlockToBlockchain段代碼,再次運行
blockchain.AddBlockToBlockchain("4th Block")blockchain.AddBlockToBlockchain("5th Block")
mainTest_3
我們看到,在原有區塊資訊不變的情況,新挖出的區塊成功添加到了區塊鏈資料庫中。說明我們的區塊鏈資料持久化實現成功了。
.
.
.
.
互連網顛覆世界,區塊鏈顛覆互連網!
---------------------------------------------20180624 20:16