從0到1簡易區塊鏈開發手冊V0.3-數據持久化與創世區塊

Author: brucefenggit

Email: brucefeng@brucefeng.comgithub

編程語言:Golang
從0到1簡易區塊鏈開發手冊V0.3-數據持久化與創世區塊數據庫


1.BoltDB簡介

Bolt是一個純粹Key/Value模型的程序。該項目的目標是爲不須要完整數據庫服務器(如Postgres或MySQL)的項目提供一個簡單,快速,可靠的數據庫。編程

BoltDB只須要將其連接到你的應用程序代碼中便可使用BoltDB提供的API來高效的存取數據。並且BoltDB支持徹底可序列化的ACID事務,讓應用程序能夠更簡單的處理複雜操做。數組

其源碼地址爲:https://github.com/boltdb/bolt服務器

2.BoltDB特性

BoltDB設計源於LMDB,具備如下特色:網絡

  • 使用Go語言編寫
  • 不須要服務器便可運行
  • 支持數據結構數據結構

  • 直接使用API存取數據,沒有查詢語句;
  • 支持徹底可序列化的ACID事務,這個特性比LevelDB強;
  • 數據保存在內存映射的文件裏。沒有wal、線程壓縮和垃圾回收;
  • 經過COW技術,可實現無鎖的讀寫併發,可是沒法實現無鎖的寫寫併發,這就註定了讀性能超高,但寫性能通常,適合與讀多寫少的場景。

BoltDB是一個Key/Value(鍵/值)存儲,這意味着沒有像SQL RDBMS(MySQL,PostgreSQL等)中的表,沒有行,沒有列。相反,數據做爲鍵值對存儲(如在Golang Maps中)。鍵值對存儲在Buckets中,它們旨在對類似的對進行分組(這與RDBMS中的表相似)。所以,爲了得到Value(值),須要知道該Value所在的桶和鑰匙。併發

3.BoltDB簡單使用

//經過go get下載並import 
import "github.com/boltdb/bolt"

3.1 打開或建立數據庫

db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
   log.Fatal(err)
}
defer db.Close()
  • 執行注意點

若是經過goland程序運行建立的my.db會保存在編程語言

GOPATH /src/Project目錄下
若是經過go build main.go ; ./main 執行生成的my.db,會保存在當前目錄GOPATH /src/Project/package下

3.2 數據庫操做

(1) 建立數據庫表與數據寫入操做
//1. 調用Update方法進行數據的寫入
err = db.Update(func(tx *bolt.Tx) error {
//2.經過CreateBucket()方法建立BlockBucket(表),初次使用建立
   b, err := tx.CreateBucket([]byte("BlockBucket"))
   if err != nil {
      return fmt.Errorf("Create bucket :%s", err)
   }

//3.經過Put()方法往表裏面存儲一條數據(key,value),注意類型必須爲[]byte
   if b != nil {
      err := b.Put([]byte("l"), []byte("Send $100 TO Bruce"))
      if err != nil {
         log.Panic("數據存儲失敗..")
      }
   }

   return nil
})

//數據Update失敗,退出程序
if err != nil {
   log.Panic(err)
}
(2) 數據寫入
//1.打開數據庫
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
   log.Fatal(err)
}
defer db.Close()

err = db.Update(func(tx *bolt.Tx) error {

//2.經過Bucket()方法打開BlockBucket表
   b := tx.Bucket([]byte("BlockBucket"))
//3.經過Put()方法往表裏面存儲數據
   if b != nil {
      err := b.Put([]byte("l"), []byte("Send $200 TO Fengyingcong"))
      err =  b.Put([]byte("ll"), []byte("Send $100 TO Bruce"))
      if err != nil {
         log.Panic("數據存儲失敗..")
      }
   }

   return nil
})
//更新失敗
if err != nil {
   log.Panic(err)
}
(3) 數據讀取
//1.打開數據庫
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
   log.Fatal(err)
}
defer db.Close()

//2.經過View方法獲取數據
err = db.View(func(tx *bolt.Tx) error {

//3.打開BlockBucket表,獲取表對象

   b := tx.Bucket([]byte("BlockBucket"))

//4.Get()方法經過key讀取value
   if b != nil {
      data := b.Get([]byte("l"))
      fmt.Printf("%s\n", data)
      data = b.Get([]byte("ll"))
      fmt.Printf("%s\n", data)
   }

   return nil
})

if err != nil {
   log.Panic(err)
}

4.經過BoltDB存儲區塊

該代碼包含對BoltDB的數據庫建立,表建立,區塊添加,區塊查詢操做

//1.建立一個區塊對象block
block := BLC.NewBlock("Send $500 to Tom", 1, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})

//2. 打印區塊對象相關信息
fmt.Printf("區塊的Hash信息爲:\t%x\n", block.Hash)
fmt.Printf("區塊的數據信息爲:\t%v\n", string(block.Data))
fmt.Printf("區塊的隨機數爲:\t%d\n", block.Nonce)

//3. 打開數據庫
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
   log.Fatal(err)
}
defer db.Close()

//4. 更新數據
err = db.Update(func(tx *bolt.Tx) error {

//4.1 打開BlockBucket表對象
   b := tx.Bucket([]byte("blocks"))
//4.2 若是表對象不存在,建立表對象
   if b == nil {
      b, err = tx.CreateBucket([]byte("blocks"))
      if err != nil {
         log.Panic("Block Table Create Failed")
      }
   }

   //4.3 往表裏面存儲一條數據(key,value)
   err = b.Put([]byte("l"), block.Serialize())
   if err != nil {
      log.Panic("數據存儲失敗..")
   }
   return nil
})

//更新失敗,返回錯誤
if err != nil {
   log.Panic("數據更新失敗")
}

//5. 查看數據
err = db.View(func(tx *bolt.Tx) error {

//5.1打開BlockBucket表對象
   b := tx.Bucket([]byte("blocks"))

   if b != nil {
       //5.2 取出key=「l」對應的value
      blockData := b.Get([]byte("l"))
        //5.3反序列化   
      block := BLC.DeserializeBlock(blockData)
 //6. 打印區塊對象相關信息
    fmt.Printf("區塊的Hash信息爲:\t%x\n", block.Hash)
    fmt.Printf("區塊的數據信息爲:\t%v\n", string(block.Data))
    fmt.Printf("區塊的隨機數爲:\t%d\n", block.Nonce)
   }

   return nil
})
//數據查看失敗
if err != nil {
   log.Panic("數據更新失敗")
}

五.建立創世區塊

從0到1簡易區塊鏈開發手冊V0.3-數據持久化與創世區塊

1.概念

北京時間2009年1月4日2時15分5秒,比特幣的第一個區塊誕生了。隨着時間日後推移,不斷有新的區塊被添加到鏈上,全部後續區塊均可以追溯到第一個區塊。第一個區塊就被人們稱爲創世區塊。

2. 工做量證實

在比特幣世界中,獲取區塊記帳權的過程稱之爲挖礦,一個礦工成功後,他會把以前打包好的網絡上的交易記錄到一頁帳本上,同步給其餘人。由於這個礦工可以最早計算出超難數學題的正確答案,說明這個礦工付出了工做量,是一個有權利記帳的人,所以其餘人也會贊成這一頁帳單。這種依靠工做量來證實記帳權,你們來達成共識的機制叫作「工做量證實」,簡而言之結果能夠證實你付出了多少工做量。Proof Of Work簡稱「PoW」,關於其原理跟代碼實現,咱們在後面的代碼分析中進行講解說明。

2.1 定義結構體

type ProofOfWork struct {
    Block  *Block   //要驗證的block
    Target *big.Int //目標hash
}

2.2 建立工做量證實對象

const TargetBit = 16 //目標哈希的0個個數,16,20,24,28
func NewProofOfWork(block *Block) *ProofOfWork {
    //1.建立pow對象
    pow := &ProofOfWork{}
    //2.設置屬性值
    pow.Block = block
    target := big.NewInt(1)           // 目標hash,初始值爲1
    target.Lsh(target, 256-TargetBit) //左移256-16
    pow.Target = target
    return pow

}

咱們首先設定一個難度係數值爲16,即目標哈希前導0的個數,0的個數越多,挖礦難度越大,此處咱們建立一個函數NewProofOfWork用於返回Pow對象。

目標Hash的長度爲256bit,經過64個16進制byte進行展現,以下所示爲前導0爲16/4=4的哈希

0000c01d342fc51cb030f93979343de70ab771855dd8ca28e6f5888737759747
  • 經過big.NewInt建立一個BigInt對象target
  • 對target進行經過左移(256-TargetBit)位操做

2.3 將int64類型轉[]byte

func IntToHex(num int64) []byte {
    buff := new(bytes.Buffer)
    //將二進制數據寫入w
    //
    err := binary.Write(buff, binary.BigEndian, num)
    if err != nil {
        log.Panic(err)
    }
    //轉爲[]byte並返回
    return buff.Bytes()
}

經過func Write(w io.Writer, order ByteOrder, data interface{}) error方法將一個int64的整數轉爲二進制後,每8bit一個byte,轉爲[]byte

2.4 拼接區塊屬性數據

func (pow *ProofOfWork) prepareData(nonce int64) []byte {
    data := bytes.Join([][]byte{
        IntToHex(pow.Block.Height),
        pow.Block.PrevBlockHash,
        IntToHex(pow.Block.TimeStamp),
        pow.Block.HashTransactions(),
        IntToHex(nonce),
        IntToHex(TargetBit),
    }, []byte{})
    return data

}

經過bytes.Join方法將區塊相關屬性進行拼接成字節數組

2.5 "挖礦"方法

func (pow *ProofOfWork) Run() ([]byte, int64) {

    var nonce int64 = 0
    var hash [32]byte
    for {
        //1.根據nonce獲取數據
        data := pow.prepareData(nonce)
        //2.生成hash
        hash = sha256.Sum256(data) //[32]byte
        fmt.Printf("\r%d,%x", nonce, hash)
        //3.驗證:和目標hash比較
        /*
        func (x *Int) Cmp(y *Int) (r int)
        Cmp compares x and y and returns:

           -1 if x <  y
            0 if x == y
           +1 if x >  y
        目的:target > hashInt,成功
         */
        hashInt := new(big.Int)
        hashInt.SetBytes(hash[:])

        if pow.Target.Cmp(hashInt) == 1 {
            break
        }

        nonce++
    }
    fmt.Println()
    return hash[:], nonce

}

代碼思路

  • 設置nonce值:0,1,2.......
  • block-->拼接數組,產生hash
  • 比較實際hash和pow的目標hash

不斷更改nonce的值,計算hash,直到小於目標hash。

2.6 驗證區塊

func (pow *ProofOfWork) IsValid() bool {
   hashInt := new(big.Int)
   hashInt.SetBytes(pow.Block.Hash)
   return pow.Target.Cmp(hashInt) == 1
}

判斷方式同挖礦中的策略

3.區塊建立

3.1 定義結構體

type Block struct {
    //字段屬性
    //1.高度:區塊在區塊鏈中的編號,第一個區塊也叫創世區塊,通常設定爲0
    Height int64
    //2.上一個區塊的Hash值
    PrevBlockHash []byte
    //3.數據:Txs,交易數據
    Txs []*Transaction
    //4.時間戳
    TimeStamp int64
    //5.本身的hash
    Hash []byte
    //6.Nonce
    Nonce int64
}

關於屬性的定義,在代碼的註釋中比較清晰了,須要提一下的就是創世區塊的PrevBlockHash通常設定爲0 ,高度也通常設定爲0

3.2 建立創世區塊

func CreateGenesisBlock(txs []*Transaction) *Block{

    return NewBlock(txs,make([]byte,32,32),0)
}

設定創世區塊的PrevBlockHash爲0,區塊高度爲0

3.3 序列化區塊對象

func (block *Block) Serialize()[]byte{
    //1.建立一個buff
    var buf bytes.Buffer

    //2.建立一個編碼器
    encoder:=gob.NewEncoder(&buf)

    //3.編碼
    err:=encoder.Encode(block)
    if err != nil{
        log.Panic(err)
    }

    return buf.Bytes()
}

經過gob庫的Encode方法將Block對象序列化成字節數組,用於持久化存儲

3.4 字節數組反序列化

func DeserializeBlock(blockBytes [] byte) *Block{
    var block Block
    //1.先建立一個reader
    reader:=bytes.NewReader(blockBytes)
    //2.建立×××
    decoder:=gob.NewDecoder(reader)
    //3.解碼
    err:=decoder.Decode(&block)
    if err != nil{
        log.Panic(err)
    }
    return &block
}

定義一個函數,用於將[]byte反序列化爲block對象

4.區塊鏈建立

4.1 定義結構體

type BlockChain struct {
    DB  *bolt.DB //對應的數據庫對象
    Tip [] byte  //存儲區塊中最後一個塊的hash值
}

定義區塊鏈結構體屬性DB用於存儲對應的數據庫對象,Tip用於存儲區塊中最後一個塊的Hash值

4.2 判斷數據庫是否存在

const DBName  = "blockchain.db" //數據庫的名字
const BlockBucketName = "blocks" //定義bucket

定義數據庫名字以及定義用於存儲區塊數據的bucket(表)名

func dbExists() bool {
    if _, err := os.Stat(DBName); os.IsNotExist(err) {  
        return false //表示文件不存在
    }
    return true //表示文件存在
}

須要注意IsNotExist返回true,則表示不存在成立,返回值爲true,則dbExists函數的返回值則須要返回false,不然,返回true

4.3 建立帶有創世區塊的區塊鏈

func CreateBlockChainWithGenesisBlock(address string) {

    /*
    1.判斷數據庫若是存在,直接結束方法
    2.數據庫不存在,建立創世區塊,並存入到數據庫中
     */
    if dbExists() {
        fmt.Println("數據庫已經存在,沒法建立創世區塊")
        return
    }

    //數據庫不存在
    fmt.Println("數據庫不存在")
    fmt.Println("正在建立創世區塊")
    /*
    1.建立創世區塊
    2.存入到數據庫中
     */
    //建立一個txs--->CoinBase
    txCoinBase := NewCoinBaseTransaction(address)

    genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})
    db, err := bolt.Open(DBName, 0600, nil)
    if err != nil {
        log.Panic(err)
    }
    defer db.Close()

    err = db.Update(func(tx *bolt.Tx) error {
        //創世區塊序列化後,存入到數據庫中
        b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))
        if err != nil {
            log.Panic(err)
        }

        if b != nil {
            err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())
            if err != nil {
                log.Panic(err)
            }
            b.Put([]byte("l"), genesisBlock.Hash)
        }
        return nil
    })
    if err != nil {
        log.Panic(err)
    }
    //return &BlockChain{db, genesisBlock.Hash}
}

代碼分析

(1) 判斷數據庫是否存在,若是不存在,證實尚未建立創世區塊,若是存在,則提示創世區塊已存在,直接返回

if dbExists() {
        fmt.Println("數據庫已經存在,沒法建立創世區塊")
        return
    }

(2) 若是數據庫不存在,則提示開始調用相關函數跟方法建立創世區塊

fmt.Println("數據庫不存在")
    fmt.Println("正在建立創世區塊")

(3) 建立一個交易數組Txs

關於交易這一部份內容,將在後面一個章節中進行詳細說明,篇幅會很是長,這也是整個課程體系中最爲繁瑣,知識點最廣的地方,屆時慢慢分析

txCoinBase := NewCoinBaseTransaction(address)

經過函數NewCoinBaseTransaction建立一個CoinBase交易

func NewCoinBaseTransaction(address string) *Transaction {
   txInput := &TxInput{[]byte{}, -1, nil, nil}
   txOutput := NewTxOutput(10, address)
   txCoinBaseTransaction := &Transaction{[]byte{}, []*TxInput{txInput}, []*TxOutput{txOutput}}
   //設置交易ID
   txCoinBaseTransaction.SetID()
   return txCoinBaseTransaction
}

(4) 生成創世區塊

genesisBlock := CreateGenesisBlock([]*Transaction{txCoinBase})

(5) 打開/建立數據庫

db, err := bolt.Open(DBName, 0600, nil)
    if err != nil {
        log.Panic(err)
    }
    defer db.Close()

經過bolt.Open方法打開(若是不存在則建立)數據庫文件,注意數據庫關閉操做不能少,用defer實現延遲關閉。

(6) 將數據寫入數據庫

err = db.Update(func(tx *bolt.Tx) error {
        b, err := tx.CreateBucketIfNotExists([]byte(BlockBucketName))
        if err != nil {
            log.Panic(err)
        }

        if b != nil {
            err = b.Put(genesisBlock.Hash, genesisBlock.Serialize())
            if err != nil {
                log.Panic(err)
            }
            b.Put([]byte("l"), genesisBlock.Hash)
        }
        return nil
    })
    if err != nil {
        log.Panic(err)
    }

經過db.Upadate方法進行數據更新操做

  • 建立/打開存儲區塊的Bucket:BlockBucketName
  • 將創世區塊序列化後存入Bucket中
    • 經過Put方法更新K/V值(Key:區塊哈希,Value:區塊序列化後的字節數組)
    • 經過Put方法更新Key爲「l」的Value爲最新區塊哈希值,此處即genesisBlock.Hash

5.命令行調用

func (cli *CLI) CreateBlockChain(address string) {
    CreateBlockChainWithGenesisBlock(address)
}

測試命令

$ ./mybtc  createblockchain -address 1DHPNHKfk9uUdog2f2xBvx9dq4NxpF5Q4Q

返回結果

數據庫不存在
正在建立創世區塊
32325,00005c7b4246aa88bd1f9664c665d6424d1522f569d981691ac2b5b5d15dd8d9

本章節介紹瞭如何建立一個帶有創世區塊的區塊鏈,並持久化存儲至數據庫blockchain.db

$ ls
BLC             Wallets.dat     blockchain.db   main.go         mybtc
相關文章
相關標籤/搜索