使用Go構建區塊鏈 第3部分:持久化和cli

銀河1號發表於2019-01-27

Introduction

到目前為止,我們已經建立了一個帶有工作量證明系統的區塊鏈,這使得挖礦成為可能。我們的實現越來越接近功能齊全的區塊鏈,但它仍然缺乏一些重要的功能。今天將開始在資料庫中儲存區塊鏈,之後我們將建立一個簡單的命令列介面來執行區塊鏈操作。從本質上講,區塊鏈是一個分散式資料庫。我們暫時將省略“分散式”部分,並專注於“資料庫”部分。

Database Choice

目前,我們的實現中沒有資料庫;相反,我們每次執行程式時都會建立塊並將它們儲存在記憶體中。我們無法重用區塊鏈,我們無法與其他人共享,因此我們需要將其儲存在磁碟上。

我們需要哪個資料庫?實際上,任何一個資料庫都可以。在the original Bitcoin paper沒有任何關於使用某個資料庫的說法,因此由開發人員決定使用哪個資料庫。Bitcoin Core,最初由中本聰釋出,目前是比特幣的參考實現,使用LevelDB。而我們將要使用的是....

BoltDB

因為:

  1. 它足夠簡單。
  2. 它使用Go實現。
  3. 它不需要執行伺服器。
  4. 它允許構建我們想要的資料結構。

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

在開始實現永續性邏輯之前,我們首先需要決定如何在資料庫中儲存資料。為此,我們將參考比特幣核心的方式。

簡單來說,比特幣核心使用兩個“桶”來儲存資料:

  1. blocks儲存描述鏈中所有塊的後設資料。
  2. chainstate儲存鏈的狀態,這是當前未使用的事務輸出和一些後設資料。

此外,塊作為單獨的檔案儲存在磁碟上。這樣做是出於效能目的:讀取單個塊不需要將所有(或部分)塊載入到記憶體中。我們不會實現這一點。

blocks 中,key -> value 為:

keyvalue
b + 32 位元組的 block hashblock index record
f + 4 位元組的 file numberfile information record
l + 4 位元組的 file numberthe last block file number used
R + 1 位元組的 boolean是否正在 reindex
F + 1 位元組的 flag name length + flag name string1 byte boolean: various flags that can be on or off
t + 32 位元組的 transaction hashtransaction index record

chainstatekey -> value 為:

keyvalue
c + 32 位元組的 transaction hashunspent transaction output record for that transaction
B32 位元組的 block hash: the block hash up to which the database represents the unspent transaction outputs

詳情可見 這裡

由於我們還沒有交易,我們將只有blocks桶。另外,如上所述,我們將整個DB儲存為單個檔案,而不將塊儲存在單獨的檔案中。所以我們不需要任何與檔案編號相關的內容。最終,我們會用到的鍵值對有:

  1. 32 位元組的 block-hash -> block 結構
  2. l -> 鏈中最後一個塊的 hash

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

Serialization

如前所述,在BoltDB中,值只能是[]byte型別,我們想儲存Block資料庫中的結構。我們會用的encoding/gob序列化結構。

讓我們來實現 BlockSerialize 方法(為了簡潔起見,此處略去了錯誤處理):

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 例項,並向其中加入創世塊。而現在,我們希望它做的事情有:

  1. 開啟一個資料庫檔案
  2. 檢查是否存在區塊鏈。
  3. 如果有區塊鏈:
    1. 建立一個新的 Blockchain 例項
    2. 設定 Blockchain 例項的 tip 為資料庫中儲存的最後一個塊的雜湊
  4. 如果沒有現有的區塊鏈:
    1. 建立創世塊。
    2. 儲存到資料庫
    3. 將g創世塊的雜湊儲存為最後一個塊雜湊。
    4. 建立一個新的 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

到目前為止,我們的實現還沒有提供任何與程式互動的介面:我們只是執行了NewBlockchainbc.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")
複製程式碼

首先,我們建立兩個子命令: addblockprintchain, 然後給 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

(未經同意,請勿轉載)



相關文章