本篇是"用Go構(gòu)建區(qū)塊鏈"系列的第三篇,主要對(duì)原文進(jìn)行翻譯。對(duì)應(yīng)原文如下:
https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
1、介紹
到目前為止,我們已經(jīng)構(gòu)建了一個(gè)帶有工作量證明的區(qū)塊鏈,這使得挖礦稱為可能。我們的實(shí)現(xiàn)越來越接近功能完整的區(qū)塊鏈,但它仍然缺乏一些重要功能。今天將開始在數(shù)據(jù)庫中存儲(chǔ)區(qū)塊鏈,之后我們將制作一個(gè)簡(jiǎn)單的命令行界面來執(zhí)行區(qū)塊鏈操作。本質(zhì)上,區(qū)塊鏈?zhǔn)且粋€(gè)分布式數(shù)據(jù)庫。現(xiàn)在我們將省略"分布式"部分,并專注于"數(shù)據(jù)庫"部分。
2、數(shù)據(jù)庫選擇
目前,我們實(shí)現(xiàn)的區(qū)塊鏈中沒有數(shù)據(jù)庫; 相反,我們每次運(yùn)行程序時(shí)都會(huì)創(chuàng)建區(qū)塊并將它們存儲(chǔ)在內(nèi)存中。我們不能重復(fù)使用區(qū)塊鏈,我們無法與其他人共享,因此我們需要將其存儲(chǔ)在磁盤上。
我們需要哪個(gè)數(shù)據(jù)庫?實(shí)際上,他們中的任何一個(gè)都可以。在最初的比特幣原始論文中,關(guān)于使用某個(gè)數(shù)據(jù)庫沒有任何說法,因此它完全取決于開發(fā)者如何選擇。 Bitcoin Core,最初由中本聰發(fā)布,也是目前比特幣實(shí)現(xiàn)的參考版本。它使用LevelDB(盡管它僅在2012年引入客戶端)。我們將使用...
3、BoltDB
因?yàn)椋?/p>
1.它簡(jiǎn)單而很??;
2.它用Go語言實(shí)現(xiàn);
3.它不需要運(yùn)行服務(wù)器;
4.它允許構(gòu)建我們想要的數(shù)據(jù)結(jié)構(gòu)。
以下摘自 BoltDB在 Github的README :
Bolt是一個(gè)純Go語言寫的Key/Value存儲(chǔ),受到了Howard Chu的LMDB項(xiàng)目的啟發(fā)。該項(xiàng)目的目標(biāo)是為不需要完整數(shù)據(jù)庫服務(wù)器(如Postgres或MySQL)的項(xiàng)目提供一個(gè)簡(jiǎn)單,快速且可靠的數(shù)據(jù)庫。
由于Bolt旨在用作這種低級(jí)別的功能,因此簡(jiǎn)單性是關(guān)鍵。該API將很小,只專注于獲取值和設(shè)置值。僅此而已。
聽起來非常適合我們的需求!讓我們花一分鐘審查一下。
BoltDB是一個(gè)鍵/值存儲(chǔ),這意味著沒有像SQL RDBMS(MySQL,PostgreSQL等)那樣的表??,沒有行,沒有列。相反,數(shù)據(jù)存儲(chǔ)為鍵值對(duì)(如Golang中的map)。鍵值對(duì)存儲(chǔ)在桶(bucket)中,這些桶(bucket)用于將類似的鍵值對(duì)進(jìn)行分組(這與RDBMS中的表類似)。因此,為了獲得價(jià)值,你需要知道一個(gè)桶(bucket)和一個(gè)鍵(key)。
關(guān)于BoltDB的一個(gè)重要的事情是它沒有數(shù)據(jù)類型:鍵和值是字節(jié)數(shù)組(byte arrays)。由于我們將Go結(jié)構(gòu)(也就是區(qū)塊 Block )存儲(chǔ)在其中,因此我們需要將它們序列化,即實(shí)現(xiàn)將Go結(jié)構(gòu)轉(zhuǎn)換為字節(jié)數(shù)組并將其從字節(jié)數(shù)組恢復(fù)的機(jī)制。我們將使用 encoding/gob 做這件事情。不過,JSON , XML,Protocol Buffers 等均可使用。我們使用 encoding/gob 是因?yàn)樗芎?jiǎn)單,并且是標(biāo)準(zhǔn)Go庫的一部分。
4、數(shù)據(jù)庫結(jié)構(gòu)
在開始實(shí)施持久性邏輯之前,我們首先需要決定如何將數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫中。為此,我們將介紹比特幣核心的做法。
簡(jiǎn)而言之,Bitcoin Core使用兩個(gè)"桶(buckets)"來存儲(chǔ)數(shù)據(jù):
-
blocks存儲(chǔ)描述鏈中所有塊的元數(shù)據(jù)。 -
chainstate存儲(chǔ)鏈的狀態(tài),這是目前所有未使用的事務(wù)輸出和一些元數(shù)據(jù)。
另外,區(qū)塊在磁盤上作為單獨(dú)的文件存儲(chǔ)。這是為了達(dá)到性能目的而完成的:讀取單個(gè)塊不需要將全部(或部分)全部加載到內(nèi)存中。我們不會(huì)實(shí)現(xiàn)這個(gè)。
在 blocks,key -> value 對(duì)是:
1.'b' + 32 字節(jié)的區(qū)塊 hash -> 區(qū)塊索引記錄
2.'f' + 4 字節(jié)的文件數(shù)字 -> 文件信息記錄
3.'l' -> 4 字節(jié)的文件數(shù)字:最后一個(gè)使用過的區(qū)塊文件數(shù)字
4.'R' -> 1 字節(jié)的布爾值: 我們是否要去重新索引
5.'F' + 1 字節(jié)的標(biāo)志名長(zhǎng)度 + 標(biāo)志名字符串 -> 1 字節(jié)布爾值: 開或關(guān)的多種標(biāo)志
6.'t' + 32 字節(jié)的交易hash -> 交易索引記錄
在 chainstate,key -> value 對(duì)是:
1.'c' + 32 字節(jié)的交易hash -> 未使用的交易出賬記錄
2.'B' -> 32 字節(jié)的區(qū)塊hash: 數(shù)據(jù)庫應(yīng)該表示的未使用交易出賬的區(qū)塊哈希
(詳細(xì)解釋可以在這里找到)
因?yàn)槲覀儠簳r(shí)并沒有交易信息,我們可以只有一個(gè) blocks 桶。另外,如上所述,我們將整個(gè)數(shù)據(jù)庫存儲(chǔ)為單個(gè)文件,而不將區(qū)塊存儲(chǔ)在單獨(dú)的文件中。所以我們不需要任何與文件編號(hào)相關(guān)的東西。所以這些是我們將要使用的 key -> value(鍵值)對(duì):
1.32 字節(jié)的區(qū)塊hash -> 區(qū)塊結(jié)構(gòu)(序列化后的)
2.'l’ -> 鏈上最后一個(gè)區(qū)塊的 hash
這就是我們開始實(shí)現(xiàn)持久性機(jī)制所需要知道的。
5、序列化
如前所述,BoltDB中的值只能是 []byte 類型,而我們想要將 Block 結(jié)構(gòu)存儲(chǔ)在數(shù)據(jù)庫中。我們將使用encoding/gob來序列化結(jié)構(gòu)。
讓我們來實(shí)現(xiàn) Serialize 的方法 Block(錯(cuò)誤處理此不再贅述):
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
這篇文章很簡(jiǎn)單:首先,我們聲明一個(gè)將存儲(chǔ)序列化數(shù)據(jù)的緩沖區(qū); 然后我們初始化一個(gè) gob 編碼器并對(duì)區(qū)塊進(jìn)行編碼; 結(jié)果以字節(jié)數(shù)組的形式返回。
接下來,我們需要一個(gè)反序列化函數(shù),它將接收一個(gè)字節(jié)數(shù)組作為輸入并返回一個(gè) Block 。這不是一個(gè)方法,而是一個(gè)獨(dú)立的功能:
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
這就是序列化!
6、持久化
我們從這個(gè)NewBlockchain函數(shù)開始。現(xiàn)在,它創(chuàng)建一個(gè) Blockchain 的實(shí)例,并添加一個(gè)創(chuàng)世區(qū)塊到里面。我們想做的是這樣的:
1.打開一個(gè)數(shù)據(jù)庫文件
2.檢查是否存在區(qū)塊鏈
3.如果有區(qū)塊鏈:
- 創(chuàng)建一個(gè)新的
Blockchain實(shí)例。 - 將
Blockchain實(shí)例的提示設(shè)置為存儲(chǔ)在數(shù)據(jù)庫中的最后一個(gè)區(qū)塊哈希。
4.如果不存在區(qū)塊鏈:
- 創(chuàng)建創(chuàng)世區(qū)塊
- 存儲(chǔ)在數(shù)據(jù)庫中
- 將創(chuàng)世區(qū)塊的哈希保存為最后一個(gè)區(qū)塊的哈希
- 創(chuàng)建一個(gè)新的
Blockchain實(shí)例,其指向創(chuàng)世區(qū)塊
在代碼中,它看起來像這樣:
func NewBlockchain() *Blockchain {
var tip []byte
db, err := bolt.Open(dbFile, 0600, nil)
err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
return nil
})
bc := Blockchain{tip, db}
return &bc
}
讓我們來一段一段地來回顧一下。
db, err := bolt.Open(dbFile, 0600, nil)
這是打開BoltDB文件的標(biāo)準(zhǔn)方式。注意,如果沒有這樣的文件,它不會(huì)返回錯(cuò)誤。
err = db.Update(func(tx *bolt.Tx) error {
...
})
在BoltDB中,數(shù)據(jù)庫操作在事務(wù)(transaction)中運(yùn)行。有兩種類型的事務(wù):只讀和讀寫。在這里,我們打開一個(gè)讀寫事務(wù)(db.Update(...)),因?yàn)槲覀兿M麑?chuàng)世區(qū)塊放在數(shù)據(jù)庫中。
b := tx.Bucket([]byte(blocksBucket))
if b == nil {
genesis := NewGenesisBlock()
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
err = b.Put([]byte("l"), genesis.Hash)
tip = genesis.Hash
} else {
tip = b.Get([]byte("l"))
}
這是該函數(shù)的核心。在這里,我們獲得桶(bucket)來存儲(chǔ)我們的區(qū)塊:如果存在,我們讀 l 鍵; 如果不存在,就生成創(chuàng)世塊,創(chuàng)建bucket,將區(qū)塊保存到其中,并更新 l 鍵來存儲(chǔ)鏈中的最后一個(gè)區(qū)塊的哈希。
另外,注意一下創(chuàng)建 Blockchain 的新方式:
bc := Blockchain{tip, db}
我們不再存儲(chǔ)所有的區(qū)塊了,而只存儲(chǔ)鏈的頂端。另外,我們存儲(chǔ)一個(gè)數(shù)據(jù)庫的連接。因?yàn)槲覀兿胍坏┐蜷_它,就在程序運(yùn)行時(shí)保持打開狀態(tài)。因此, Blockchain 結(jié)構(gòu)現(xiàn)在看起來像這樣:
type Blockchain struct {
tip []byte
db *bolt.DB
}
接下來我們要更新的 AddBlock 方法:現(xiàn)在向鏈中添加區(qū)塊并不像向數(shù)組中添加元素那么簡(jiǎn)單。從現(xiàn)在開始,我們將在DB中存儲(chǔ)區(qū)塊:
func (bc *Blockchain) AddBlock(data string) {
var lastHash []byte
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
newBlock := NewBlock(data, lastHash)
err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
return nil
})
}
讓我們來一段一段地來回顧一下:
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
lastHash = b.Get([]byte("l"))
return nil
})
這是另一種(只讀)類型的BoltDB事務(wù)。在這里,我們從數(shù)據(jù)庫中獲取最后一個(gè)區(qū)塊哈希來使用它來挖掘一個(gè)新的區(qū)塊哈希。
newBlock := NewBlock(data, lastHash)
b := tx.Bucket([]byte(blocksBucket))
err := b.Put(newBlock.Hash, newBlock.Serialize())
err = b.Put([]byte("l"), newBlock.Hash)
bc.tip = newBlock.Hash
挖掘新塊后,我們將其序列化表示保存到數(shù)據(jù)庫中,并更新 l 的鍵(key),它現(xiàn)在存儲(chǔ)了新區(qū)塊的哈希。
完成!這并不難,是嗎?
7、檢查區(qū)塊鏈
所有新塊現(xiàn)在都保存在數(shù)據(jù)庫中,因此我們可以重新打開區(qū)塊鏈并為其添加新塊。但是在實(shí)現(xiàn)這個(gè)之后,我們失去了一個(gè)重要的缺陷:我們不能再打印出區(qū)塊鏈塊,因?yàn)槲覀儾辉賹K存儲(chǔ)在數(shù)組中。讓我們來修復(fù)這個(gè)問題!
BoltDB允許迭代桶(bucket)中的所有鍵,不過這些鍵是以字節(jié)排序的順序存儲(chǔ),我們希望區(qū)塊按照它們?cè)趨^(qū)塊鏈中的順序進(jìn)行打印。另外,因?yàn)槲覀儾幌雽⑺袇^(qū)塊加載到內(nèi)存中(我們的區(qū)塊鏈數(shù)據(jù)庫可能很大,或者我們假裝它可以),我們將逐個(gè)讀取它們。為此,我們需要一個(gè)區(qū)塊鏈迭代器:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
每次我們想要遍歷區(qū)塊鏈中的區(qū)塊時(shí),都會(huì)創(chuàng)建一個(gè)迭代器,它將存儲(chǔ)當(dāng)前迭代的區(qū)塊哈希和到數(shù)據(jù)庫的連接。因?yàn)楹笳撸粋€(gè)迭代器在邏輯上被依加到區(qū)塊鏈的(它是一個(gè) Blockchain 存儲(chǔ)數(shù)據(jù)庫連接的實(shí)例),所以了,我們?cè)?Blockchain 里創(chuàng)建方法:
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
請(qǐng)注意,迭代器最初指向區(qū)塊鏈的頂端(tip),因此區(qū)塊將從上到下,從最新到最舊獲取。事實(shí)上,選擇提示意味著區(qū)塊鏈的"投票"。區(qū)塊鏈可以有多個(gè)分支,并且它們中被認(rèn)為是最長(zhǎng)的分支。在找到頂端(tip)(可以是區(qū)塊鏈中的任何區(qū)塊)后,我們可以重新構(gòu)建整個(gè)區(qū)塊鏈并計(jì)算其長(zhǎng)度以及構(gòu)建區(qū)塊鏈所需的工作。這個(gè)事實(shí)意味著,一個(gè)頂端(tip)也就是區(qū)塊鏈的一種標(biāo)識(shí)符。
BlockchainIterator 只會(huì)做一件事:它會(huì)從區(qū)塊鏈返回下一個(gè)區(qū)塊。
func (i *BlockchainIterator) Next() *Block {
var block *Block
err := i.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
encodedBlock := b.Get(i.currentHash)
block = DeserializeBlock(encodedBlock)
return nil
})
i.currentHash = block.PrevBlockHash
return block
}
這就是數(shù)據(jù)庫部分!
8、CLI(命令行)
到現(xiàn)在為止我們的實(shí)現(xiàn)并沒有提供任何接口與程序交互:我們只是執(zhí)行 NewBlockchain ,bc.AddBlock 在 main 函數(shù)。是時(shí)候來改善這一點(diǎn)了!我們想要這些命令:
blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain
有的命令行依賴的操作都會(huì)被在 CLI 結(jié)構(gòu)中進(jìn)行:
type CLI struct {
bc *Blockchain
}
它的 "入口" 是 Run 函數(shù):
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:])
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
default:
cli.printUsage()
os.Exit(1)
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
我們使用標(biāo)準(zhǔn)的 flag 包來解析命令行參數(shù)。
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
首先,我們創(chuàng)建了兩個(gè)子命令, addblock 和 printchain ,我們?cè)偌尤?-data 標(biāo)志前者。 printchain 將不會(huì)有任何標(biāo)志。
switch os.Args[1] {
case "addblock":
err := addBlockCmd.Parse(os.Args[2:])
case "printchain":
err := printChainCmd.Parse(os.Args[2:])
default:
cli.printUsage()
os.Exit(1)
}
接下來我們檢查用戶提供的命令并解析相關(guān)的 flag 子命令。
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
接下來我們檢查哪些子命令被解析并運(yùn)行相關(guān)函數(shù)。
cli.bc.AddBlock(data)
fmt.Println("Success!")
}
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)
pow := NewProofOfWork(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevBlockHash) == 0 {
break
}
}
}
這部分內(nèi)容和我們之前的內(nèi)容很像,唯一的區(qū)別是我們現(xiàn)在使用的是 BlockchainIterator 遍歷區(qū)塊鏈中的區(qū)塊:
我們也不要忘記修改相應(yīng)的 main 函數(shù):
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}
請(qǐng)注意,Blockchain 無論提供什么命令行參數(shù),都會(huì)創(chuàng)建一個(gè)新的鏈。
就是這樣!讓我們來檢查一切是否按預(yù)期工作:
$ blockchain_go printchain
No existing blockchain found. Creating a new one...
Mining the block containing "Genesis Block"
000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
$ blockchain_go addblock -data "Send 1 BTC to Ivan"
Mining the block containing "Send 1 BTC to Ivan"
000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Success!
$ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee"
Mining the block containing "Pay 0.31337 BTC for a coffee"
000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
Success!
$ blockchain_go printchain
Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
Data: Pay 0.31337 BTC for a coffee
Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148
PoW: true
Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
Data: Send 1 BTC to Ivan
Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13
PoW: true
Prev. hash:
Data: Genesis Block
Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b
PoW: true
(啤酒的聲音可以打開)
9、總結(jié)
下次我們將實(shí)現(xiàn)地址,錢包和(可能還有)交易。敬請(qǐng)期待!
鏈接:
1.完整的源代碼
2.Bitcoin Core數(shù)據(jù)存儲(chǔ)
3.boltdb
4.encoding/gob
5.flag
由于水平有限,翻譯質(zhì)量不太好,歡迎大家拍磚。