持久化和命令行接口
已經(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()
}