Introduction
到目前為止,我們已經建立了一個帶有工作量證明系統的區塊鏈,這使得挖礦成為可能。我們的實現越來越接近功能齊全的區塊鏈,但它仍然缺乏一些重要的功能。今天將開始在資料庫中儲存區塊鏈,之後我們將建立一個簡單的命令列介面來執行區塊鏈操作。從本質上講,區塊鏈是一個分散式資料庫。我們暫時將省略“分散式”部分,並專注於“資料庫”部分。
Database Choice
目前,我們的實現中沒有資料庫;相反,我們每次執行程式時都會建立塊並將它們儲存在記憶體中。我們無法重用區塊鏈,我們無法與其他人共享,因此我們需要將其儲存在磁碟上。
我們需要哪個資料庫?實際上,任何一個資料庫都可以。在the original Bitcoin paper沒有任何關於使用某個資料庫的說法,因此由開發人員決定使用哪個資料庫。Bitcoin Core,最初由中本聰釋出,目前是比特幣的參考實現,使用LevelDB。而我們將要使用的是....
BoltDB
因為:
- 它足夠簡單。
- 它使用Go實現。
- 它不需要執行伺服器。
- 它允許構建我們想要的資料結構。
Bolt是一個純粹的Go鍵/值儲存,受到Howard Chu的LMDB專案的啟發。該專案的目標是為不需要完整資料庫伺服器(如Postgres或MySQL)的專案提供簡單,快速,可靠的資料庫。
由於Bolt旨在用作這種低階功能,因此簡單性是關鍵。 API很小,只關注獲取值和設定值。而已。
聽起來非常適合我們的需求!我們花點時間回顧一下。
BoltDB是一個鍵/值儲存,這意味著沒有像SQL RDBMS(MySQL,PostgreSQL等)中的表,沒有行,沒有列。相反,資料儲存為鍵值對(如Golang對映中)。鍵值對儲存在儲存桶中,儲存桶用於對類似的對進行分組(這類似於RDBMS中的表)。因此,為了獲得一個值,您需要知道一個桶和一個金鑰。
BoltDB的一個重要特點是沒有資料型別:鍵和值是位元組陣列。鑑於需要在裡面儲存 Go 的結構(準確來說,也就是儲存Block(塊)),我們需要對它們進行序列化,即實現一種將Go結構轉換為位元組陣列並從位元組陣列中恢復的機制。我們會用的encoding/gob , 不過 JSON, XML, Protocol Buffers等也可以使用。我們正在使用encoding/gob因為它很簡單,是標準Go庫的一部分。
Database Structure
在開始實現永續性邏輯之前,我們首先需要決定如何在資料庫中儲存資料。為此,我們將參考比特幣核心的方式。
簡單來說,比特幣核心使用兩個“桶”來儲存資料:
- blocks儲存描述鏈中所有塊的後設資料。
- chainstate儲存鏈的狀態,這是當前未使用的事務輸出和一些後設資料。
此外,塊作為單獨的檔案儲存在磁碟上。這樣做是出於效能目的:讀取單個塊不需要將所有(或部分)塊載入到記憶體中。我們不會實現這一點。
在 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桶。另外,如上所述,我們將整個DB儲存為單個檔案,而不將塊儲存在單獨的檔案中。所以我們不需要任何與檔案編號相關的內容。最終,我們會用到的鍵值對有:
- 32 位元組的 block-hash -> block 結構
l
-> 鏈中最後一個塊的 hash
這就是實現持久化機制所有需要了解的內容了。
Serialization
如前所述,在BoltDB中,值只能是[]byte型別,我們想儲存Block資料庫中的結構。我們會用的encoding/gob序列化結構。
讓我們來實現 Block
的 Serialize
方法(為了簡潔起見,此處略去了錯誤處理):
func (b *Block) Serialize() []byte {
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
err := encoder.Encode(b)
return result.Bytes()
}
複製程式碼
這個部分很簡單:首先,我們宣告一個儲存序列化資料的緩衝區;然後我們初始化一個gob編碼器和編碼塊;結果以位元組陣列的形式返回。
接下來,我們需要一個反序列化函式,它將接收一個位元組陣列作為輸入並返回一個Block。這不是一種方法,而是一種獨立的功能:
func DeserializeBlock(d []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(d))
err := decoder.Decode(&block)
return &block
}
複製程式碼
這就是序列化!
Persistence
讓我們從 NewBlockchain
函式開始。在之前的實現中,NewBlockchain
會建立一個新的 Blockchain
例項,並向其中加入創世塊。而現在,我們希望它做的事情有:
- 開啟一個資料庫檔案
- 檢查是否存在區塊鏈。
- 如果有區塊鏈:
- 建立一個新的
Blockchain
例項 - 設定
Blockchain
例項的 tip 為資料庫中儲存的最後一個塊的雜湊 - 如果沒有現有的區塊鏈:
- 建立創世塊。
- 儲存到資料庫
- 將g創世塊的雜湊儲存為最後一個塊雜湊。
- 建立一個新的
Blockchain
例項,初始時 tip 指向創世塊(tip 有尾部,尖端的意思,在這裡 tip 儲存的是最後一個塊的雜湊)
在程式碼中,它看起來像這樣:
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檔案的標準方法。請注意,如果沒有此類檔案,它將不會返回錯誤。
err = db.Update(func(tx *bolt.Tx) error {
...
})
複製程式碼
在BoltDB中,使用資料庫的操作在事務中執行。有兩種型別的事務:只讀和讀寫。在這裡,我們開啟一個讀寫事務(db.Update(...)),因為我們希望將創世塊放在DB中。
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"))
}
複製程式碼
這是該功能的核心。在這裡,我們獲取儲存塊的儲存桶:如果存在,我們讀取l鍵;如果它不存在,我們生成創世塊,建立桶,將塊儲存到其中,並更新l金鑰儲存鏈的最後一個塊雜湊。
另外,注意建立 Blockchain
一個新的方式:
bc := Blockchain{tip, db}
複製程式碼
我們不再儲存其中的所有塊,而是僅儲存鏈的尖端。此外,我們儲存資料庫連線,因為我們想要開啟它一次並在程式執行時保持開啟狀態。就這樣Blockchain結構現在看起來像這樣:
type Blockchain struct {
tip []byte
db *bolt.DB
}
複製程式碼
接下來我們要更新的是AddBlock方法:現在向鏈中新增塊並不像向陣列中新增元素那麼容易。從現在開始,我們將在資料庫中儲存塊:
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事務的另一種(只讀)型別。在這裡,我們從DB獲取最後一個塊雜湊,以使用它來挖掘新的塊雜湊。
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
複製程式碼
在挖掘新塊之後,我們將其序列化表示儲存到DB中並更新l鍵,現在儲存新塊的雜湊值。
完成!這不難,是嗎?
Inspecting Blockchain
所有新塊現在都儲存在資料庫中,因此我們可以重新開啟區塊鏈並向其新增新塊。但是在實現之後,我們失去了一個很好的功能:我們不能再列印出區塊鏈塊了,因為我們不再將塊儲存在陣列中。讓我們解決這個缺陷吧!
BoltDB允許迭代桶中的所有鍵,但鍵按位元組排序順序儲存,我們希望塊按照它們在區塊鏈中的順序列印。另外,因為我們不想將所有塊載入到記憶體中(我們的區塊鏈資料庫可能很大!或者只是假裝它可以),我們將逐一閱讀它們。為此,我們需要一個區塊鏈迭代器:
type BlockchainIterator struct {
currentHash []byte
db *bolt.DB
}
複製程式碼
每當要對鏈中的塊進行迭代時,我們就會建立一個迭代器,裡面儲存了當前迭代的塊雜湊(currentHash
)和資料庫的連線(db
)。通過 db
,迭代器邏輯上被附屬到一個區塊鏈上(這裡的區塊鏈指的是儲存了一個資料庫連線的 Blockchain
例項),並且通過 Blockchain
方法進行建立:
func (bc *Blockchain) Iterator() *BlockchainIterator {
bci := &BlockchainIterator{bc.tip, bc.db}
return bci
}
複製程式碼
請注意,迭代器最初指向區塊鏈的頂端,因此將從上到下,從最新到最舊獲得塊。事實上,選擇一個tip意味著對區塊鏈進行“投票”。區塊鏈可以有多個分支,並且它們中最長的被認為是主要分支。獲得tip後(它可以是區塊鏈中的任何塊)我們可以重建整個區塊鏈並找到它的長度和構建它所需的工作。這同樣也意味著,一個 tip 也就是區塊鏈的一種識別符號。
BlockchainIterator
只會做一件事情:返回鏈中的下一個塊。
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
}
複製程式碼
這就是資料庫部分!
CLI
到目前為止,我們的實現還沒有提供任何與程式互動的介面:我們只是執行了NewBlockchain
和 bc.AddBlock
。是時候改善了!我們想要這些命令:
blockchain_go addblock "Pay 0.031337 for a coffee"
blockchain_go printchain複製程式碼
所有與命令列相關的操作都將由CLI struct處理:
type CLI struct {
bc *Blockchain
}
複製程式碼
它的 “入口” 是 Run
函式:
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()
}
}
複製程式碼
我們正在使用該標準flag包解析命令列引數。
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError)
addBlockData := addBlockCmd.String("data", "", "Block data")
複製程式碼
首先,我們建立兩個子命令: addblock
和 printchain
, 然後給 addblock
新增 -data
標誌。printchain
沒有任何標誌。
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)
}
複製程式碼
然後,我們檢查使用者提供的命令,解析相關的 flag
子命令:
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
os.Exit(1)
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
複製程式碼
接著檢查解析是哪一個子命令,並呼叫相關函式:
func (cli *CLI) addBlock(data string) {
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
}
}
}
複製程式碼
這件作品與我們之前的作品非常相似。唯一的區別是我們現在正在使用BlockchainIterator迭代區塊鏈中的塊。
記得不要忘了對 main
函式作出相應的修改:
func main() {
bc := NewBlockchain()
defer bc.db.Close()
cli := CLI{bc}
cli.Run()
}複製程式碼
注意,無論提供什麼命令列引數,都會建立一個新的鏈。
這就是今天的所有內容了! 來看一下是不是如期工作:
$ 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
複製程式碼
英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
更多文章歡迎訪問 http://www.apexyun.com/
聯絡郵箱:public@space-explore.com
(未經同意,請勿轉載)