1.前言
到目前為止我們了解區(qū)塊鏈的數(shù)據(jù)結(jié)構(gòu)以及簡易版的挖礦(pow共識機(jī)制)。接下來我們將一起了解區(qū)塊鏈的存儲憎蛤,注意:區(qū)塊鏈本質(zhì)上一款分布式數(shù)據(jù)庫龄毡,這里不實(shí)現(xiàn)分布式,我們這先了解區(qū)塊鏈存儲部分朵逝。
2.知識準(zhǔn)備
知識點(diǎn) | 學(xué)習(xí)網(wǎng)頁 | 特性 |
---|---|---|
比特幣數(shù)據(jù)庫 | leveldb | 1.key和value都是任意長度的字節(jié)數(shù)組;2.entry(即一條K-V記錄)默認(rèn)是按照key的字典順序存儲的乡范,當(dāng)然開發(fā)者也可以重載這個排序函數(shù)配名;3.提供的基本操作接口:Put()、Delete()晋辆、Get()渠脉、Batch();4.支持批量操作以原子操作進(jìn)行瓶佳;5.可以創(chuàng)建數(shù)據(jù)全景的snapshot(快照)芋膘,并允許在快照中查找數(shù)據(jù);6.可以通過前向(或后向)迭代器遍歷數(shù)據(jù)(迭代器會隱含的創(chuàng)建一個snapshot)霸饲;7.自動使用Snappy壓縮數(shù)據(jù)为朋;8、可移植性厚脉; |
BoltDB數(shù)據(jù)庫 | boltDB | 1.它簡單而簡約;2.它在Go中實(shí)現(xiàn);3.它不需要運(yùn)行服務(wù)器;4.它允許構(gòu)建我們想要的數(shù)據(jù)結(jié)構(gòu)习寸。 |
go中序列化 | 由于數(shù)據(jù)庫是字節(jié)碼的方式存儲這里我們需要序列化對象,采用encoding/gob包 |
這里我們將會用道boltdb數(shù)據(jù)庫來存儲我們的數(shù)據(jù)傻工。
3.數(shù)據(jù)結(jié)構(gòu)
我們看看比特幣數(shù)據(jù)庫是怎么存儲的霞溪。
簡單理解,比特幣使用了兩個"buckets"(桶)來存儲數(shù)據(jù):
- blocks 描述鏈上所有區(qū)塊的元數(shù)據(jù)孵滞。
- chainstate 存儲區(qū)塊鏈的狀態(tài),指的是當(dāng)前所有的UTXO(未花費(fèi)交易輸出)以及一些元數(shù)據(jù)鸯匹。
"在比特幣的世界里既沒有賬戶坊饶,也沒有余額,只有分散到區(qū)塊鏈里的UTXO"
另外殴蓬,塊在磁盤上作為單獨(dú)的文件存儲匿级。 這是為了達(dá)到性能目的而完成的:讀取單個塊不需要將全部(或部分)全部加載到內(nèi)存中。 我們不會執(zhí)行這個染厅。
在blocks這個桶中,存儲的是鍵值對:
#塊索引記
'b' + 32-byte block hash -> block index record
#文件信息記錄
'f' + 4-byte file number -> file information record
#使用的最后一個塊文件編號
'l' -> 4-byte file number: the last block file number used
#是否處于重建索引的進(jìn)程當(dāng)中
'R' -> 1-byte boolean: whether we're in the process of reindexing
#各種可以打開或關(guān)閉的flag標(biāo)志
'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off
#交易索引記錄
't' + 32-byte transaction hash -> transaction index record
在 chainstate 這個桶中根蟹,存儲的鍵值對:
#某筆交易的UTXO記錄
'c' + 32-byte transaction hash -> unspent transaction output record for that transaction
#數(shù)據(jù)庫表示未使用的事務(wù)輸出的塊散列
'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs
由于我們還沒有交易,因此我們只會封裝bucket糟秘。 另外简逮,如上所述,我們將整個DB存儲為單個文件尿赚,而不將塊存儲在單獨(dú)的文件中散庶。 所以我們不需要任何與文件編號有關(guān)的東西。 因此凌净,這些是我們將使用的鍵 - >值對:
#區(qū)塊數(shù)據(jù)與區(qū)塊hash的鍵值對
32-byte block-hash -> Block structure (serialized)
#鏈中最后一個塊的散列
'l' -> the hash of the last block in a chain
4.序列化
由于這里Key與Value采用[]byte的形式存儲,所以我們需要序列化,采用Go提供的encoding/gob來實(shí)現(xiàn)序列化與反序列化悲龟。
//序列化Block
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
if err != nil {
log.Panic(err)
}
return result.Bytes()
}
//反序列化
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
}
5.存儲區(qū)塊數(shù)據(jù)流程圖
代碼實(shí)現(xiàn):
// 創(chuàng)建一個新的區(qū)塊鏈和創(chuàng)世塊
func NewBlockchain() *Blockchain {
var tip []byte
//打開數(shù)據(jù)庫
db, err := bolt.Open(dbFile, 0600, nil)
if err != nil {
log.Panic(err)
}
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
fmt.Println("No existing blockchain found. Creating a new one...")
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
if err != nil {
log.Panic(err)
}
err = b.Put(genesis.Hash, genesis.Serialize())
if err != nil {
log.Panic(err)
}
err = b.Put([]byte("l"), genesis.Hash)
if err != nil {
log.Panic(err)
}
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
if err != nil {
log.Panic(err)
}
bc := Blockchain{tip, db}
return &bc
}
這是打開BoltDB文件的標(biāo)準(zhǔn)方式。 注意冰寻,如果沒有這樣的文件须教,它不會返回錯誤。
添加區(qū)塊方法AddBlock:現(xiàn)在我們添加區(qū)塊并不會向數(shù)組添加元素那么簡單斩芭,我們將block存儲在DB中:
//添加區(qū)塊
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte //最后一個區(qū)塊hash
//查詢數(shù)據(jù)庫中最后一塊的hash
err :=bc.db.View(func(tx *bolt.Tx) error {
b :=tx.Bucket([]byte(blocksBucket))
lastHash=b.Get([]byte("1"))//最新的一塊hash的key我們知道為"l"
return nil
})
if err!=nil{
log.Panic(err)
}
//利最后的一塊hash轻腺,挖掘一塊新的區(qū)塊出來
newBlock :=NewBlock(data,lastHash)
//在挖掘新塊之后,我們將其序列化表示保存到數(shù)據(jù)塊中并更新"l"划乖,該密鑰現(xiàn)在存儲新塊的哈希贬养。
err=bc.db.Update(func(tx *bolt.Tx) error {
b :=tx.Bucket([]byte(blocksBucket))
err :=b.Put(newBlock.Hash,newBlock.Serialize())
if err!=nil{
log.Panic(err)
}
err = b.Put([]byte("l"), newBlock.Hash)
if err != nil {
log.Panic(err)
}
bc.tip = newBlock.Hash
return nil
})
}
好了存儲區(qū)塊桶實(shí)現(xiàn)了,接下來我們想實(shí)現(xiàn)查看區(qū)塊鏈的區(qū)塊數(shù)據(jù)琴庵。
6.檢索區(qū)塊鏈
BoltDB允許迭代桶中的所有鍵误算,但鍵以字節(jié)排序的順序存儲,我們希望塊按照它們在區(qū)塊鏈中的順序進(jìn)行打印迷殿。另外儿礼,因?yàn)槲覀儾幌雽⑺袎K加載到內(nèi)存中(我們的區(qū)塊鏈數(shù)據(jù)庫可能很大,或者我們假裝它可以)庆寺,我們將逐個讀取它們蚊夫。為此,我們需要一個區(qū)塊鏈迭代器:
// 區(qū)塊鏈迭代器用于迭代區(qū)塊
type BlockchainIterator struct {
currentHash []byte //當(dāng)前的hash
db *bolt.DB //數(shù)據(jù)庫
}
每次我們想要遍歷區(qū)塊鏈中的塊時止邮,都會創(chuàng)建一個迭代器这橙,它將存儲當(dāng)前迭代的塊散列和到數(shù)據(jù)庫的連接。由于后者导披,迭代器在邏輯上被附加到區(qū)塊鏈(它是一個Blockchain存儲數(shù)據(jù)庫連接的實(shí)例)屈扎,因此在一個Blockchain方法中創(chuàng)建:
//迭代器
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
BlockchainIterator 只會做一件事:它會從區(qū)塊鏈返回下一個區(qū)塊。
// 迭代下一區(qū)塊
func (i *BlockchainIterator) Next() *Block {
var block *Block
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
//查詢區(qū)塊
encodedBlock := b.Get(i.currentHash)
block = DeserializeBlock(encodedBlock)
return nil
})
if err != nil {
log.Panic(err)
}
//將前一個區(qū)塊
i.currentHash = block.PrevBlockHash
return block
}
7.CLI
目前為止我們并沒有提供任何接口與程序交互撩匕,都是通過main函數(shù)里面來調(diào)用方法鹰晨,我們想通過命令的方式來執(zhí)行這些方法。封裝一個cli:
type CLI struct {
bc *block.Blockchain //區(qū)塊鏈
}
提供一個接口供main調(diào)用接口止毕。
//啟動接口函數(shù)
func Start(bc *block.Blockchain)interface{} {
cl := CLI{bc}
cl.run()//執(zhí)行命令方法
return nil
}
//打印用法
func (cli *CLI) printUsage() {
fmt.Println("Usage:")
fmt.Println(" addblock -data BLOCK_DATA - add a block to the blockchain")
fmt.Println(" printchain - print all the blocks of the blockchain")
}
//校驗(yàn)參數(shù)
func (cli *CLI) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
os.Exit(1)
}
}
//添加區(qū)塊數(shù)據(jù)
func (cli *CLI) addBlock(data string) {
cli.bc.AddBlock(data)
fmt.Println("Success!")
}
//打印區(qū)塊鏈上所有區(qū)塊數(shù)據(jù)
func (cli *CLI) printChain() {
bci := cli.bc.Iterator()
for {
block := bci.Next()
fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
fmt.Printf("PoW: %s\n", strconv.FormatBool(block.Validate()))
fmt.Println()
//創(chuàng)世塊是沒有前一個區(qū)塊的模蜡,所以PrevBlockHash的值是沒有的
if len(block.PrevBlockHash) == 0 {
break
}
}
}
// 執(zhí)行命令方法
func (cli *CLI) run() {
cli.validateArgs()//校驗(yàn)參數(shù)
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.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
修改main
func main() {
bc := block.NewBlockchain()
defer block.Close(bc)
cli.Start(bc)
}
構(gòu)建go項(xiàng)目命令:
C:\go-worke\src\github.com\study-bitcion-go>go build github.com/study-bitcion-go
命令方式添加一個數(shù)據(jù):
C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go addblock -data "even send tom 1.000000BTC"
Mining the block containing "even send tom 1.000000BTC"
Dig into mine 0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635
Success!
迭代區(qū)塊鏈數(shù)據(jù):
C:\go-worke\src\github.com\study-bitcion-go>study-bitcion-go printchain
Prev. hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
Data: even send tom 1.000000BTC
Hash: 0000042bec2da2fc8a2b1aebabd0a855d93b46d5f512356d385d744a95edd635
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 00000220260f77c875a787d79c61e2b16307914895a417438a7809b9dc7f9fb4
PoW: true
本章實(shí)現(xiàn)了數(shù)據(jù)持久化存儲,命令方式啟動扁凛。后續(xù)我們將實(shí)現(xiàn)錢包忍疾、交易、網(wǎng)絡(luò)等谨朝。
資料
- 原文來源:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
- 本文源碼:https://github.com/Even521/study-bitcion-go/tree/part3
- java學(xué)習(xí):http://www.reibang.com/p/66c065018c7a
- 區(qū)塊鏈基礎(chǔ)視頻學(xué)習(xí):https://www.bilibili.com/video/av19620321/
- 區(qū)塊鏈測試demo:https://anders.com/blockchain/blockchain.html
- 區(qū)塊鏈Q(jìng)Q交流群:489512556