打造公鏈(3)

持久化和命令行接口

已經(jīng)構(gòu)建出一個PoW機制的區(qū)塊鏈呐舔,但區(qū)塊鏈的數(shù)據(jù)需要持久化到一個數(shù)據(jù)庫漆羔,還需要提供一個簡單的命令行接口奇瘦,用戶完成一些
與區(qū)塊鏈的交互操作章咧,既然認(rèn)為區(qū)塊鏈本質(zhì)上是一個分布式數(shù)據(jù)庫,那么就要完成存儲和讀取弯洗。

選擇數(shù)據(jù)庫

原則上旅急,選用什么數(shù)據(jù)都是可以的,但go語言我們選擇BoltDB

BoltDB

簡潔
go實現(xiàn)
不需要運行一個服務(wù)器
能夠允許構(gòu)造想要的數(shù)據(jù)結(jié)構(gòu)
Bolt使用鍵值存儲涂召,沒有像SQL RDBMS的表坠非,沒有行和列。數(shù)據(jù)被存儲為鍵值對果正。鍵值對被存儲在bucket中炎码,這是為了將相似的鍵值對
進行分組。因此秋泳,為了獲取一個值潦闲,需要知道一個bucket和一個鍵(key)。

Bolt數(shù)據(jù)庫沒有數(shù)據(jù)類型:鍵和值都是字節(jié)數(shù)組(byte array)迫皱。需要存儲go的Block歉闰,就需要進行序列化。實現(xiàn)一個從go struct轉(zhuǎn)換
到一個byte array的機制卓起,同時還要能轉(zhuǎn)回struct和敬。在這里我們選用go標(biāo)準(zhǔn)庫 encoding/gob來完成這一目標(biāo)。

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

在進行序列化存儲之前戏阅,我們想要搞明白昼弟,到底什么數(shù)據(jù)存儲到數(shù)據(jù)庫中。

首先奕筐,我們看看Bitcoin是如何做的:

Bitcoin Core使用兩個“bucket”來存儲數(shù)據(jù):

blocks舱痘,存儲了描述一條鏈中所有塊的元數(shù)據(jù)
chainstate,存儲了一條鏈的狀態(tài)离赫,當(dāng)前所有的未花費和交易輸出芭逝,和一些元數(shù)據(jù)
在此版輪子中,還未進行交易渊胸,只需要blocks bucket旬盯。也簡單將整個數(shù)據(jù)庫存儲為單個文件,而沒有將區(qū)塊存儲在不同的文件中蹬刷。
也不需要文件編號(file number)相關(guān)的東西瓢捉,我們會用到的鍵值對有:

32字節(jié)的block-hash -> block結(jié)構(gòu)
l -> 鏈中最后一個塊的hash

持久化

從前一個輪子的NewBlockchain函數(shù)開始,此函數(shù)創(chuàng)建一個新的區(qū)塊鏈實例办成,并且會添加一個創(chuàng)世塊泡态。加入數(shù)據(jù)庫功能后,我們希望
做更多的事情:

1.打開一個數(shù)據(jù)庫文件
2.檢查文件里面是否已經(jīng)存儲了一個區(qū)塊鏈
3.如果已經(jīng)存儲了一個區(qū)塊鏈:

  • 創(chuàng)建一個新的Blockchain實例
  • 設(shè)置Blockchain實例的tip為數(shù)據(jù)庫中存儲的最后一個塊的哈希
    4.如果沒有區(qū)塊鏈
  • 創(chuàng)建創(chuàng)世塊
  • 存儲到數(shù)據(jù)庫
  • 將創(chuàng)世塊哈希保存為最后一個塊的哈希
  • 創(chuàng)建一個新的Blockchain實例迂卢,初始時tip指向創(chuàng)世塊

檢查區(qū)塊鏈

首先構(gòu)造一個能遍歷區(qū)塊的區(qū)塊鏈迭代器(BlockchainIterator)某弦,迭代器的初始狀態(tài)為鏈中的tip桐汤,然后從尾到頭(創(chuàng)世塊)進行
迭代獲取區(qū)塊。實際上靶壮,選擇一個tip就是意味著給一條鏈“投票”怔毛,怎么解釋呢,一條鏈可能有很多分支腾降,最長的那條鏈會被認(rèn)為
是主分支拣度,獲得一個tip(可以是鏈中任意一個塊)后,就可以重新構(gòu)造整條鏈螃壤,所以說抗果,一個tip就是區(qū)塊鏈的一種標(biāo)識符。

命令行接口

說有相關(guān)命令在這個版本的輪子中奸晴,都會通過CLI struct進行處理

type CLI struct{
    blockchain *Blockchain
}

go 命令

go build -o blockchain_go

./blockchain_go printchain
./blockchain_go addblock -data "Send 1 BTC to Tom"
./blockchain_go printchain

項目代碼分為下面幾個部分

block.go

package main

import (
    "bytes"
    "encoding/gob"
    "log"
    "time"
)

type Block struct {
    Timestamp   int64
    Data    []byte
    PrevBlockHash   []byte
    Hash    []byte
    Nonce   int
}

// 將Block序列化為一個字節(jié)數(shù)組
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()
}
// 將字節(jié)數(shù)組反序列化為一個Block
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
}

func NewBlock(data string,prevBlockHash []byte) *Block {
    block := &Block{
        Timestamp:time.Now().Unix(),
        Data:[]byte(data),
        PrevBlockHash:prevBlockHash,
        Hash:[]byte{},
        Nonce:0}
    pow := NewProofOfWork(block)
    nonce,hash := pow.Run()
    block.Hash = hash[:]
    block.Nonce = nonce
    return block
}

func NewGenesisBlock() *Block {
    return NewBlock("Genesis Block", []byte{})
}

blockchain.go

package main

import (
    "github.com/boltdb/bolt"
    "log"
    "fmt"
)

const dbFile = "blockchain.db"
const blocksBucket = "blocks"

// tip 尾部的意思冤馏,這里是存儲最后一個塊的hash值 ,存儲最后的tip就能推導(dǎo)出整條chain
// 在鏈的尾端可能會短暫分叉的情況,所以選擇tip其實是選擇那條鏈
// db 存儲數(shù)據(jù)庫連接
type Blockchain struct {
    tip []byte
    db *bolt.DB
}

func NewBlockchain() *Blockchain{
    var tip []byte
    // 打開一個BoltDB文件
    db,err := bolt.Open(dbFile,0600,nil)
    if err!=nil{
        log.Panic(err)
    }
    err = db.Update(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        // 如果數(shù)據(jù)庫中不存在區(qū)塊鏈就創(chuàng)建一個寄啼,否則直接讀取最后一個塊的hash值
        if bucket==nil{
            fmt.Println("No existing blockchain found. Creating a new one...")
            genesis := NewGenesisBlock()
            bucket,err := tx.CreateBucket([]byte(blocksBucket))
            if err!=nil{
                log.Panic(err)
            }
            err = bucket.Put(genesis.Hash, genesis.Serialize())
            if err!=nil{
                log.Panic(err)
            }
            err = bucket.Put([]byte("1"),genesis.Hash)
            if err!=nil{
                log.Panic(err)
            }
            tip = genesis.Hash
        }else{
            tip = bucket.Get([]byte("1"))
        }
        return nil
    })
    if err != nil {
        log.Panic(err)
    }
    blockchain := Blockchain{tip,db}
    return &blockchain
}

// 加入?yún)^(qū)塊時逮光,需要將區(qū)塊持久化到數(shù)據(jù)庫中
func (blockchain *Blockchain) AddBlock(data string){
    var lastHash []byte
    // 首先獲取最后一個塊的哈希用于生成新的哈希
    err := blockchain.db.View(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        lastHash = bucket.Get([]byte("1"))
        return nil
    })
    if err != nil {
        log.Panic(err)
    }
    newBlock := NewBlock(data,lastHash)
    err = blockchain.db.Update(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        err := bucket.Put(newBlock.Hash,newBlock.Serialize())
        if err != nil {
            log.Panic(err)
        }
        err = bucket.Put([]byte("1"),newBlock.Hash)
        if err != nil {
            log.Panic(err)
        }
        blockchain.tip = newBlock.Hash
        return nil
    })
}

type BlockchainIterator struct {
    currentHash []byte
    db  *bolt.DB
}

func (blockchain *Blockchain) Iterator() *BlockchainIterator{
    blockchainiterator := &BlockchainIterator{blockchain.tip,blockchain.db}
    return blockchainiterator
}

// 返回鏈中的下一個塊
func (i *BlockchainIterator) Next() *Block{
    var block *Block
    err := i.db.View(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        encodedBlock := bucket.Get(i.currentHash)
        block = DeserializeBlock(encodedBlock)
        return nil
    })
    if err!=nil{
        log.Panic(err)
    }
    i.currentHash = block.PrevBlockHash
    return block
}

cli.go

package main

import (
    "fmt"
    "os"
    "flag"
    "log"
)

type CLI struct {
    blockchain *Blockchain
}
const usage = `
Usage:
    addblock -data BLOCK_DATA   add a block to the blockchain
    printchain    print all the blocks of the blockchain
`
func (cli *CLI) printUsage(){
    fmt.Println(usage)
}
func (cli *CLI) validateArgs(){
    if len(os.Args)<2{
        cli.printUsage()
        os.Exit(1)
    }
}
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.blockchain.AddBlock(*addBlockData)
    }
    if printChainCmd.Parsed(){
        cli.printChain()
    }
}

commands.go

package main

import (
    "fmt"
    "strconv"
)

func (cli *CLI) addBlock(data string){
    cli.blockchain.AddBlock(data)
    fmt.Println("add block success!")
}
func (cli *CLI) printChain(){
    blockchainiterator := cli.blockchain.Iterator()
    for {
        block := blockchainiterator.Next()
        fmt.Printf("Prev hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()
        if len(block.PrevBlockHash) == 0{
            break
        }
    }
}

main.go

package main

func main()  {
    blockchain := NewBlockchain()
    defer blockchain.db.Close()
    cli := CLI{blockchain}
    cli.Run()
}

proofofwork.go

package main

import (
    "math"
    "math/big"
    "bytes"
    "fmt"
    "crypto/sha256"
)

const targetBits = 24
var (
    maxNonce = math.MaxInt64
)
type ProofOfWork struct {
    block *Block
    target *big.Int
}
func NewProofOfWork(block *Block) *ProofOfWork{
    target := big.NewInt(1)
    target.Lsh(target,uint(256-targetBits))
    pow := &ProofOfWork{block,target}
    return pow
}

func (pow *ProofOfWork) prepareData(nonce int) []byte{
    data := bytes.Join(
        [][]byte{
            pow.block.PrevBlockHash,
            pow.block.Data,
            IntToHex(pow.block.Timestamp),
            IntToHex(int64(targetBits)),
            IntToHex(int64(nonce)),
        },
        []byte{},
    )
    return data
}

func (pow *ProofOfWork) Run() (int,[]byte){
    var hashInt big.Int
    var hash [32]byte
    nonce := 0
    fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data)
    for nonce<maxNonce{
        data := pow.prepareData(nonce)
        hash = sha256.Sum256(data)
        hashInt.SetBytes(hash[:])
        if hashInt.Cmp(pow.target)== -1{
            fmt.Printf("\r%x", hash)
            break
        }else{
            nonce++
        }
    }
    fmt.Print("\n\n")
    return nonce,hash[:]
}

func (pow *ProofOfWork) Validate() bool{
    var hashInt big.Int
    data := pow.prepareData(pow.block.Nonce)
    hash := sha256.Sum256(data)
    hashInt.SetBytes(hash[:])
    isValid := hashInt.Cmp(pow.target)==-1
    return isValid
}

utils.go

package main

import (
    "bytes"
    "encoding/binary"
    "log"
)

// Convert an int64 to a byte array
func IntToHex(num int64) []byte{
    buff := new(bytes.Buffer)
    err := binary.Write(buff,binary.BigEndian,num)
    if err!=nil{
        log.Panic(err)
    }
    return buff.Bytes()
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市墩划,隨后出現(xiàn)的幾起案子涕刚,更是在濱河造成了極大的恐慌,老刑警劉巖乙帮,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件副女,死亡現(xiàn)場離奇詭異,居然都是意外死亡蚣旱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門戴陡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塞绿,“玉大人,你說我怎么就攤上這事恤批∫煳牵” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵喜庞,是天一觀的道長诀浪。 經(jīng)常有香客問我,道長延都,這世上最難降的妖魔是什么雷猪? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮晰房,結(jié)果婚禮上求摇,老公的妹妹穿的比我還像新娘射沟。我一直安慰自己,他們只是感情好与境,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布验夯。 她就那樣靜靜地躺著,像睡著了一般摔刁。 火紅的嫁衣襯著肌膚如雪挥转。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天共屈,我揣著相機與錄音绑谣,去河邊找鬼。 笑死趁俊,一個胖子當(dāng)著我的面吹牛域仇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寺擂,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼暇务,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了怔软?” 一聲冷哼從身側(cè)響起垦细,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挡逼,沒想到半個月后括改,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡家坎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年嘱能,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虱疏。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡惹骂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出做瞪,到底是詐尸還是另有隱情对粪,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布装蓬,位于F島的核電站著拭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏牍帚。R本人自食惡果不足惜儡遮,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暗赶。 院中可真熱鬧峦萎,春花似錦屡久、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至详幽,卻和暖如春筛欢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唇聘。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工版姑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人迟郎。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓剥险,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宪肖。 傳聞我的和親對象是個殘疾皇子表制,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

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