摘要
本文建構了一個使用工作量證明機制(POW)的類BTC的區塊鍊。将區塊鍊持久化到一個Bolt資料庫中,然後會提供一個簡單的指令行接口,用來完成一些與區塊鍊的互動操作。這篇文章目的是希望幫助大家了解BTC源碼的架構,是以主要專注于的實作原理及存儲上,暫時忽略了 “分布式” 這個部分。嚴格來說還不能算是一個完全意義上的區塊鍊系統。
開發環境
語言:GO;
資料庫:BoltDB;
IDE: Goland或其他工具都可以;
系統:不限,本文使用windows。
BoltDB資料庫
實際上,選擇任何一個資料庫都可以,本文先用的是BoltDB。在比特币白皮書中,并沒有提到要使用哪一個具體的資料庫,它完全取決于開發者如何選擇。現在是比特币的一個參考實作,Bitcoin core使用的是是LevelDB。
BoltDB安裝及使用可以參考《BoltDB簡單使用教程》。
BoltDB有如下優點:
- 非常簡單和簡約
- 用 Go 實作
- 不需要運作一個伺服器
- 能夠允許我們構造想要的資料結構
由于 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 标準庫的一部分。
區塊鍊原型的函數架構

系統實作
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();
我們希望
:
NewBlockchain實作的功能有
- 打開一個資料庫檔案
- 檢查檔案裡面是否已經存儲了一個區塊鍊
- 如果已經存儲了一個區塊鍊:
- 建立一個新的
執行個體
Blockchain
- 設定
執行個體的 tip 為資料庫中存儲的最後一個塊的哈希
Blockchain
- 如果沒有區塊鍊:
- 建立創世塊
- 存儲到資料庫
- 将創世塊哈希儲存為最後一個塊的哈希
執行個體,初始時 tail 指向創世塊( tail存儲的是最後一個塊的哈希值)
Blockchain
- 提供一個添加區塊的方法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運算:
列印區塊鍊已有區塊資訊:
Reference:
最後要感謝Ivan Kuznetsov在GitHub社群的貢獻!