[區塊鏈BTC原理]用 Go 構建一個區塊鏈原型
摘要
本文構建了一個使用工作量證明機制(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)。
需要註意
數據庫結構
在開始實現持久化的邏輯之前,我們首先需要決定到底要如何在數據庫中進行存儲。為此,我們可以參考 Bitcoin Core 的做法:
簡單來說,Bitcoin Core 使用兩個 “bucket” 來存儲數據:
- 其中一個 bucket 是 blocks,它存儲了描述一條鏈中所有塊的元數據
- 另一個 bucket 是 chainstate,存儲了一條鏈的狀態,也就是當前所有的未花費的交易輸出,和一些元數據
此外,出於性能的考慮,Bitcoin Core 將每個區塊(block)存儲為磁盤上的不同文件。如此一來,就不需要僅僅為了讀取一個單一的塊而將所有(或者部分)的塊都加載到內存中。但是,為了簡單起見,我們並不會實現這一點。
在 blocks 中,key -> value 為:
key | value |
---|---|
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 |
在 chainstate,key -> value 為:
key | value |
---|---|
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)相關的東西。最終,我們會用到的鍵值對有:
- 32 字節的 block-hash -> block 結構
-
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 構建一個區塊鏈原型