1. 程式人生 > >[區塊鏈BTC原理]用 Go 構建一個區塊鏈原型

[區塊鏈BTC原理]用 Go 構建一個區塊鏈原型

分享 持久 簡潔 nco shu 難度 文件中 use tor

摘要

本文構建了一個使用工作量證明機制(POW)的類BTC的區塊鏈。將區塊鏈持久化到一個Bolt數據庫中,然後會提供一個簡單的命令行接口,用來完成一些與區塊鏈的交互操作。這篇文章目的是希望幫助大家理解BTC源碼的架構,所以主要專註於的實現原理及存儲上,暫時忽略了 “分布式” 這個部分。嚴格意義上還不能算是一個完全意義上的區塊鏈系統。

開發環境

語言:GO;

數據庫:BoltDB;

IDE: Goland或其他工具都可以;

系統:不限,本文使用windows。

BoltDB數據庫

實際上,選擇任何一個數據庫都可以,本文先用的是BoltDB。在比特幣白皮書中,並沒有提到要使用哪一個具體的數據庫,它完全取決於開發者如何選擇。現在是比特幣的一個參考實現,Bitcoin core使用的是是LevelDB。

BoltDB安裝及使用可以參考《BoltDB簡單使用教程》

BoltDB有如下優點:

  1. 非常簡單和簡約
  2. 用 Go 實現
  3. 不需要運行一個服務器
  4. 能夠允許我們構造想要的數據結構

由於 Bolt 意在用於提供一些底層功能,簡潔便成為其關鍵所在。它的API 並不多,並且僅關註值的獲取和設置。僅此而已。

Bolt 使用鍵值存儲,數據被存儲為鍵值對(key-value pair,就像 Golang 的 map)。鍵值對被存儲在 bucket 中,這是為了將相似的鍵值對進行分組(類似 RDBMS 中的表格)。因此,為了獲取一個值,你需要知道一個 bucket 和一個鍵(key)。

需要註意

的一個事情是,Bolt 數據庫沒有數據類型:鍵和值都是字節數組(byte array)。鑒於需要在裏面存儲 Go 的結構(準確來說,也就是存儲(塊)Block),我們需要對它們進行序列化,也就說,實現一個從 Go struct 轉換到一個 byte array 的機制,同時還可以從一個 byte array 再轉換回 Go struct。雖然我們將會使用 encoding/gob 來完成這一目標,但實際上也可以選擇使用 JSON, XML, Protocol Buffers 等等。之所以選擇使用 encoding/gob, 是因為它很簡單,而且是 Go 標準庫的一部分。

數據庫結構

在開始實現持久化的邏輯之前,我們首先需要決定到底要如何在數據庫中進行存儲。為此,我們可以參考 Bitcoin Core 的做法:

簡單來說,Bitcoin Core 使用兩個 “bucket” 來存儲數據:

  1. 其中一個 bucket 是 blocks,它存儲了描述一條鏈中所有塊的元數據
  2. 另一個 bucket 是 chainstate,存儲了一條鏈的狀態,也就是當前所有的未花費的交易輸出,和一些元數據

此外,出於性能的考慮,Bitcoin Core 將每個區塊(block)存儲為磁盤上的不同文件。如此一來,就不需要僅僅為了讀取一個單一的塊而將所有(或者部分)的塊都加載到內存中。但是,為了簡單起見,我們並不會實現這一點。

blocks 中,key -> value 為:

keyvalue
b + 32 字節的 block hash block index record
f + 4 字節的 file number file information record
l + 4 字節的 file number the last block file number used
R + 1 字節的 boolean 是否正在 reindex
F + 1 字節的 flag name length + flag name string 1 byte boolean: various flags that can be on or off
t + 32 字節的 transaction hash transaction index record

chainstatekey -> value 為:

keyvalue
c + 32 字節的 transaction hash unspent transaction output record for that transaction
B 32 字節的 block hash: the block hash up to which the database represents the unspent transaction outputs

因為目前還沒有交易,所以我們只需要 blocks bucket。另外,正如上面提到的,我們會將整個數據庫存儲為單個文件,而不是將區塊存儲在不同的文件中。所以,我們也不會需要文件編號(file number)相關的東西。最終,我們會用到的鍵值對有:

  1. 32 字節的 block-hash -> block 結構
  2. l -> 鏈中最後一個塊的 hash

這就是實現持久化機制所有需要了解的內容了。

系統實現

1.區塊文件block.go

該部分主要包括:

對區塊結構的定義;創建區塊的方法NewBlock();區塊的序列化Serialize()與反序列化Deserialize()函數;以及創世區塊的生成NewGenesisBlock()

//定義一個區塊的結構Block
type Block struct {
    //版本號
    Version int64
    //父區塊頭哈希值
    PreBlockHash []byte
    //當前區塊的Hash值, 為了簡化代碼
    Hash []byte
    //Merkle根
    MerkleRoot []byte
    //時間戳
    TimeStamp int64
    //難度值
    Bits int64
    //隨機值
    Nonce int64

    //交易信息
    Data []byte
}

//提供一個創建區塊的方法
func NewBlock(data string, preBlockHash []byte) *Block {
    var block Block
    block = Block{
        Version:      1,
        PreBlockHash: preBlockHash,
        //Hash TODO
        MerkleRoot: []byte{},
        TimeStamp:  time.Now().Unix(),
        Bits:       targetBits,
        Nonce:      0,
        Data:       []byte(data)}
    //block.SetHash()
    pow := NewProofOfWork(&block)
    nonce, hash := pow.Run()
    block.Nonce = nonce
    block.Hash = hash

    return &block
}

// 將 Block 序列化為一個字節數組
func (block *Block) Serialize() []byte {
    var buffer bytes.Buffer
    encoder := gob.NewEncoder(&buffer)

    err := encoder.Encode(block)
    CheckErr("Serialize", err)

    return buffer.Bytes()
}

// 將字節數組反序列化為一個 Block
func Deserialize(data []byte) *Block {

    if len(data) == 0 {
        return nil
    }

    var block Block

    decoder := gob.NewDecoder(bytes.NewReader(data))
    err := decoder.Decode(&block)
    CheckErr("Deserialize", err)

    return &block
}


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

2.區塊鏈blockChain.go

該部分內容主要包括:

定義一個區塊鏈結構BlockChain結構體
提供一個創建BlockChain的方法NewBlockChain()
提供一個添加區塊的方法AddBlock(data string)

叠代器對區塊進行遍歷。
const dbFile = "blockchain.db"
const blocksBucket = "bucket"
const lastHashKey = "key"

//定義一個區塊鏈結構BlockChain
type BlockChain struct {
    //blocks []*Block
    //數據庫的操作句柄
    db *bolt.DB
    //tail尾巴,表示最後一個區塊的哈希值
    //在鏈的末端可能出現短暫分叉的情況,所以選擇tail其實也就是選擇了哪條鏈
    tail []byte
}

//提供一個創建BlockChain的方法
func NewBlockChain() *BlockChain {
    // 打開一個 BoltDB 文件
    //func Open(path string, mode os.FileMode, options *Options) (*DB, error)
    db, err := bolt.Open(dbFile, 0600, nil)
    //utils中的校驗函數,校驗錯誤
    CheckErr("NewBlockChain1", err)

    var lastHash []byte

    err = db.Update(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))

        // 如果數據庫中不存在bucket,要去創建創世區塊,將數據填寫到數據庫的bucket中
        if bucket == nil {
            fmt.Println("No existing blockchain found. Creating a new one...")
            genesis := NewGenesisBlock()

            bucket, err := tx.CreateBucket([]byte(blocksBucket))
            CheckErr("NewBlockChain2", err)

            err = bucket.Put(genesis.Hash, genesis.Serialize())
            CheckErr("NewBlockChain3", err)

            err = bucket.Put([]byte(lastHashKey), genesis.Hash)
            CheckErr("NewBlockChain4", err)
            lastHash = genesis.Hash
        } else {
            //直接讀取最後區塊的哈希值
            lastHash = bucket.Get([]byte(lastHashKey))
        }

        return nil
    })

    CheckErr("db.Update", err)

    return &BlockChain{db, lastHash}
}

//提供一個添加區塊的方法
func (bc *BlockChain) AddBlock(data string) {
    var preBlockHash []byte

    err := bc.db.View(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        if bucket == nil {
            os.Exit(1)
        }

        preBlockHash = bucket.Get([]byte(lastHashKey))
        return nil
    })
    CheckErr("AddBlock-View", err)

    block := NewBlock(data, preBlockHash)
    err = bc.db.Update(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        if bucket == nil {
            os.Exit(1)
        }

        err = bucket.Put(block.Hash, block.Serialize())
        CheckErr("AddBlock1", err)

        err = bucket.Put([]byte(lastHashKey), block.Hash)
        CheckErr("AddBlock2", err)
        bc.tail = block.Hash
        return nil
    })
    CheckErr("AddBlock-Update", err)
}

//叠代器,就是一個對象,它裏面包含了一個遊標,一直向前/後移動,完成整個容器的遍歷
type BlockChainIterator struct {
    currentHash []byte
    db          *bolt.DB
}

//創建叠代器,同時初始化為指向最後一個區塊
func (bc *BlockChain) NewIterator() *BlockChainIterator {
    return &BlockChainIterator{bc.tail, bc.db}
}

// 返回鏈中的下一個塊
func (it *BlockChainIterator) Next() (block *Block) {

    err := it.db.View(func(tx *bolt.Tx) error {
        bucket := tx.Bucket([]byte(blocksBucket))
        if bucket == nil {
            return nil
        }
        data := bucket.Get(it.currentHash)
        block = Deserialize(data)
        it.currentHash = block.PreBlockHash
        return nil
    })
    CheckErr("Next", err)
    return
}

3.工作量證明機制POW.go

該部分主要包括:

創建POW的方法NewProofOfWork(block *Block)

計算哈希值的方法 Run() (int64, []byte)

//定義一個工作量證明的結構ProofOfWork
type ProofOfWork struct {
    block *Block
    //目標值
    target *big.Int
}

//難度值常量
const targetBits = 20

//創建POW的方法
func NewProofOfWork(block *Block) *ProofOfWork {

    //000000000000000... 01
    target := big.NewInt(1)
    //0x1000000000000...00
    target.Lsh(target, uint(256-targetBits))

    pow := ProofOfWork{block: block, target: target}
    return &pow
}

//給Run()準備數據
func (pow *ProofOfWork) PrepareData(nonce int64) []byte {
    block := pow.block
    tmp := [][]byte{
        /*
            需要將block中的不同類型都轉化為byte,以便進行連接
        */
        IntToByte(block.Version),
        block.PreBlockHash,
        block.MerkleRoot,
        IntToByte(block.TimeStamp),
        IntToByte(nonce),
        IntToByte(targetBits),
        block.Data}
    //func Join(s [][]byte, sep []byte) []byte
    data := bytes.Join(tmp, []byte{})
    return data
}

//計算哈希值的方法
func (pow *ProofOfWork) Run() (int64, []byte) {
    /*偽代碼
    for nonce {
        hash := sha256(block數據 + nonce)
        if 轉換(Hash)< pow.target{
            找到了
        }else{
        nonce++
        }
    }
    return nonce,hash{:}
    */
    //1.拼裝數據
    //2.哈希值轉成big.Int類型
    var hash [32]byte
    var nonce int64 = 0
    var hashInt big.Int

    fmt.Println("Begin Minding...")
    fmt.Printf("target hash :    %x\n", pow.target.Bytes())

    for nonce < math.MaxInt64 {
        data := pow.PrepareData(nonce)
        hash = sha256.Sum256(data)

        hashInt.SetBytes(hash[:])
        // Cmp compares x and y and returns:
        //
        //   -1 if x <  y
        //    0 if x == y
        //   +1 if x >  y
        //
        //func (x *Int) Cmp(y *Int) (r int) {
        if hashInt.Cmp(pow.target) == -1 {
            fmt.Printf("found hash  :%x,nonce :%d\n,", hash, nonce)
            break
        } else {
            //fmt.Printf("not found nonce,current nonce :%d,hash : %x\n", nonce, hash)
            nonce++
        }
    }
    return nonce, hash[:]
}

//校驗函數
func (pow *ProofOfWork) IsValid() bool {
    var hashInt big.Int

    data := pow.PrepareData(pow.block.Nonce)
    hash := sha256.Sum256(data)
    hashInt.SetBytes(hash[:])

    return hashInt.Cmp(pow.target) == -1
}

4.命令函交互CLI.go

註意這部分需要使用標準庫裏面的 flag 包來解析命令行參數;

首先,創建兩個子命令: addblock printchain, 然後給 addblock 添加 --data 標誌。printchain 沒有標誌;

然後,檢查用戶輸入的命令並解析相關的 flag 子命令;

最後檢查解析是哪一個子命令,並調用相關函數執行。

具體如下:

//因為是多行的,所以用反引號`···`包一下,可以實現多行字符串的拼接,不需要轉義!
//命令行提示
const usage = `
Usage:
  addBlock -data BLOCK_DATA    "add a block to the blockchain"
  printChain                   "print all the blocks of the blockchain"
`
const AddBlockCmdString = "addBlock"
const PrintChainCmdString = "printChain"

//輸出提示函數
func (cli *CLI) printUsage() {
    fmt.Println("Invalid input!")
    fmt.Println(usage)
    os.Exit(1)
}

//參數檢查函數
func (cli *CLI) validateArgs() {
    if len(os.Args) < 2 {
        fmt.Println("invalid input!")
        cli.printUsage()
    }
}

func (cli *CLI) Run() {
    cli.validateArgs()

    addBlockCmd := flag.NewFlagSet(AddBlockCmdString, flag.ExitOnError)
    printChainCmd := flag.NewFlagSet(PrintChainCmdString, flag.ExitOnError)
    //func (f *FlagSet) String(name string, value string, usage string) *string
    addBlocCmdPara := addBlockCmd.String("data", "", "Block data")

    switch os.Args[1] {
    case AddBlockCmdString:
        //添加動作
        err := addBlockCmd.Parse(os.Args[2:])
        CheckErr("Run()1", err)
        if addBlockCmd.Parsed() {
            if *addBlocCmdPara == "" {
                fmt.Println("addBlock data not should be empty!")
                cli.printUsage()
            }
            cli.AddBlock(*addBlocCmdPara)
        }
    case PrintChainCmdString:
        //打印輸出
        err := printChainCmd.Parse(os.Args[2:])
        CheckErr("Run()2", err)
        if printChainCmd.Parsed() {
            cli.PrintChain()
        }
    default:
        //命令不符合規定,輸出提示信息
        cli.printUsage()
    }
}

區塊鏈操作演示效果:

首先 go build 編譯程序;輸入不帶--data參數的錯誤命令,查看提示。

技術分享圖片

輸入交易信息,查看pow運算:

技術分享圖片

打印區塊鏈已有區塊信息:

技術分享圖片

[區塊鏈\BTC原理]用 Go 構建一個區塊鏈原型