用 Go 構建一個區塊鏈 -- Part 3: 持久化和命令列介面__教程

來源:互聯網
上載者:User

翻譯的系列文章我已經放到了 GitHub 上:blockchain-tutorial,後續如有更新都會在 GitHub 上,可能就不在這裡同步了。如果想直接運行代碼,也可以 clone GitHub 上的教程倉庫,進入 src 目錄執行 make 即可。 引言

到目前為止,我們已經構建了一個有工作量證明機制的區塊鏈。有了工作量證明,挖礦也就有了著落。雖然目前的實現離一個有著完整功能的區塊鏈越來越近了,但是它仍然缺少了一些重要的特性。在今天的內容中,我們會將區塊鏈持久化到一個資料庫中,然後會提供一個簡單的命令列介面,用來完成一些與區塊鏈的互動操作。本質上,區塊鏈是一個分散式資料庫,不過,我們暫時先忽略 “分布式” 這個部分,僅專註於 “儲存” 這一點。 選擇資料庫

目前,我們的區塊鏈實現裡面並沒有用到資料庫,而是在每次運行程式時,簡單地將區塊鏈儲存在記憶體中。那麼一旦程式退出,所有的內容就都消失了。我們沒有辦法再次使用這條鏈,也沒有辦法與其他人共用,所以我們需要把它儲存到磁碟上。

那麼,我們要用哪個資料庫呢。實際上,任何一個資料庫都可以。在 比特幣原始論文 中,並沒有提到要使用哪一個具體的資料庫,它完全取決於開發人員如何選擇。 Bitcoin Core ,最初由中本聰發布,現在是比特幣的一個參考實現,它使用的是  LevelDB。而我們將要使用的是... BoltDB

因為它: 非常簡單和簡約 用 Go 實現 不需要運行一個伺服器 能夠允許我們構造想要的資料結構

BoltDB GitHub 上的 README 是這麼說的: Bolt 是一個純KVStore for Redis的 Go 資料庫,啟發自 Howard Chu 的 LMDB. 它旨在為那些無須一個像 Postgres 和 MySQL 這樣有著完整資料庫伺服器的項目,提供一個簡單,快速和可靠的資料庫。

由於 Bolt 意在用於提供一些底層功能,簡潔便成為其關鍵所在。它的
API 並不多,並且僅關注值的擷取和設定。僅此而已。

聽起來跟我們的需求完美契合。來快速過一下:

Bolt 使用KVStore for Redis,這意味著它沒有像 SQL RDBMS (MySQL,PostgreSQL 等等)的表,沒有行和列。相反,資料被儲存為索引值對(key-value pair,就像 Golang 的 map)。索引值對被儲存在 bucket 中,這是為了將相似的索引值對進行分組(類似 RDBMS 中的表格)。因此,為了擷取一個值,你需要知道一個 bucket 和一個鍵(key)。

需要注意的一個事情是,Bolt 資料庫沒有資料類型:鍵和值都是位元組數組(byte array)。鑒於需要在裡面儲存 Go 的結構(準確來說,也就是儲存(塊)Block),我們需要對它們進行序列化,也就說,實現一個從 Go struct 轉換到一個 byte array 的機制,同時還可以從一個 byte array 再轉換回 Go struct。雖然我們將會使用  encoding/gob  來完成這一目標,但實際上也可以選擇使用 JSONXMLProtocol Buffers 等等。之所以選擇使用 encoding/gob, 是因為它很簡單,而且是 Go 標準庫的一部分。 資料庫結構

在開始實現持久化的邏輯之前,我們首先需要決定到底要如何在資料庫中進行儲存。為此,我們可以參考 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

在 chainstatekey -> 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

詳情可見 這裡:_Data_Storage)

因為目前還沒有交易,所以我們只需要 blocks bucket。另外,正如上面提到的,我們會將整個資料庫儲存為單個檔案,而不是將區Block Storage在不同的檔案中。所以,我們也不會需要檔案編號(file number)相關的東西。最終,我們會用到的索引值對有: 32 位元組的 block-hash -> block 結構 l -> 鏈中最後一個塊的 hash

這就是實現持久化機制所有需要瞭解的內容了。 序列化

上面提到,在 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()}

這個部分比較直觀:首先,我們定義一個 buffer 儲存序列化之後的資料。然後,我們初始化一個 gob encoder 並對 block 進行編碼,結果作為一個位元組數組返回。

接下來,我們需要一個解序列化的函數,它會接受一個位元組數組作為輸入,並返回一個 Block. 它不是一個方法(method),而是一個單獨的函數(function):

func DeserializeBlock(d []byte) *Block {    var block Block    decoder := gob.NewDecoder(bytes.NewReader(d))    err := decoder.Decode(&block)    return &block}

這就是序列化部分的內容了。 持久化

讓我們從 NewBlockchain 函數開始。在之前的實現中,它會建立一個新的
Blockchain 執行個體,並向其中加入創世塊。而現在,我們希望它做的事情有: 開啟一個資料庫檔案 檢查檔案裡面是否已經儲存了一個區塊鏈

如果已經儲存了一個區塊鏈: 建立一個新的 Blockchain 執行個體 設定 Blockchain 執行個體的 tip 為資料庫中儲存的最後一個塊的雜湊

如果沒有區塊鏈: 建立創世塊 儲存到資料庫 將創世塊雜湊儲存為最後一個塊的雜湊 建立一個新的 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 中,資料庫操作通過一個事務(transaction)進行操作。有兩種類型的事務:唯讀(read-only)和讀寫(read-write)。這裡,開啟的是一個讀寫事務(db.Update(...)),因為我們可能會向資料庫中添加創世塊。

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"))}

這裡是函數的核心。在這裡,我們先擷取了儲存區塊的 bucket:如果存在,就從中讀取 l 鍵;如果不存在,就產生創世塊,建立 bucket,並將區塊儲存到裡面,然後更新 l 鍵以儲存鏈中最後一個塊的雜湊。

另外,注意建立 Blockchain 一個新的方式:

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.