使用 Go 實作區塊鏈(三):建立持久化資料和命令列介面

前言

本文為「Building a Blockchain in Golang」教學影片的學習筆記。

資料庫

範例將會把區塊鏈資料存進 BadgerDB 資料庫中。BadgerDB 是一個使用 Go 編寫的可嵌入、持久且快速的鍵值對資料庫。它是 Dgraph(分佈式圖資料庫)的基礎資料庫。它打算成為 RocksDB 等非基於 Go 的鍵值存儲的高性能替代品。

實作

下載 badger 資料庫。

1
go get github.com/dgraph-io/badger/v3

Block 結構體新增一個 Serialize 方法,將其序列化為位元組切片,好將資料放進資料庫中。

1
2
3
4
5
6
7
8
func (b *Block) Serialize() []byte {
var res bytes.Buffer
encoder := gob.NewEncoder(&res)
if err := encoder.Encode(b); err != nil {
log.Fatalln(err)
}
return res.Bytes()
}

block.go 檔新增一個 Deserialize 方法,將位元組切片反序列化為一個 Block 結構體,之後從資料庫拿出資料時會使用到。

1
2
3
4
5
6
7
8
func Deserialize(data []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&block); err != nil {
log.Fatalln(err)
}
return &block
}

blockchain.go 檔建立一個 dbPath 常數,用來放置資料庫的資料。

1
2
3
const (
dbPath = "./tmp/blocks"
)

重構 BlockChain 結構體。

1
2
3
4
type BlockChain struct {
LastHash []byte // 最後一個雜湊值
Database *badger.DB // 指向資料庫的記憶體位址
}

重構 InitBlockChain 方法,判斷資料庫中是否有創世區塊,如果沒有就創建一個。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func InitBlockChain() *BlockChain {
var lastHash []byte
opts := badger.DefaultOptions(dbPath)
opts.Logger = nil
db, err := badger.Open(opts)
if err != nil {
log.Fatalln(err)
}
// 建立一個可寫的資料庫交易
err = db.Update(func(txn *badger.Txn) error {
// 如果 LastHash 不存在資料庫中,就創建創世區塊,再取得 LastHash
if _, err := txn.Get([]byte("lh")); err == badger.ErrKeyNotFound {
fmt.Println("No existing blockchain found")
// 建立一個創世區塊
genesis := Genesis()
fmt.Println("Genesis proved")
// 儲存創世區塊
if err = txn.Set(genesis.Hash, genesis.Serialize()); err != nil {
log.Fatalln(err)
}
// 更新區塊鏈的 LastHash
err = txn.Set([]byte("lh"), genesis.Hash)
// 取得區塊鏈的 LastHash
lastHash = genesis.Hash
return err
}
// 取得區塊鏈的 LastHash
item, err := txn.Get([]byte("lh"))
if err != nil {
log.Fatalln(err)
}
lastHash, err = item.ValueCopy(nil)
return err
})
if err != nil {
log.Fatalln(err)
}
return &BlockChain{lastHash, db}
}

重構 BlockChain 結構體的 AddBlock 方法,將區塊資料存進資料庫中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (chain *BlockChain) AddBlock(data string) {
var lastHash []byte
// 建立一個唯獨的資料庫交易
err := chain.Database.View(func(txn *badger.Txn) error {
// 取得區塊鏈的 LastHash
item, err := txn.Get([]byte("lh"))
if err != nil {
log.Fatalln(err)
}
lastHash, err = item.ValueCopy(nil)
return err
})
if err != nil {
log.Fatalln(err)
}
// 建立一個區塊
newBlock := CreateBlock(data, lastHash)
// 建立一個可寫的資料庫交易
err = chain.Database.Update(func(txn *badger.Txn) error {
// 存進一個區塊到資料庫中
if err := txn.Set(newBlock.Hash, newBlock.Serialize()); err != nil {
log.Fatalln(err)
}
// 更新區塊鏈的 LastHash
err = txn.Set([]byte("lh"), newBlock.Hash)
chain.LastHash = newBlock.Hash
return err
})
if err != nil {
log.Fatalln(err)
}
}

建立一個 BlockChainIterator 結構體,用來迭代區塊鏈。

1
2
3
4
type BlockChainIterator struct {
LastHash []byte // 最後一個雜湊值
Database *badger.DB // 指向資料庫的記憶體位址
}

BlockChain 結構體新增一個 Iterator 方法,回傳一個區塊鏈的迭代器。

1
2
3
func (chain *BlockChain) Iterator() *BlockChainIterator {
return &BlockChainIterator{chain.LastHash, chain.Database}
}

BlockChainIterator 結構體新增一個 Next 方法,使得迭代器能夠取得前一個區塊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (iter *BlockChainIterator) Next() *Block {
var block *Block
err := iter.Database.View(func(txn *badger.Txn) error {
item, err := txn.Get(iter.CurrentHash)
encodedBlock, err := item.ValueCopy(nil)
block = Deserialize(encodedBlock)
return err
})
if err != nil {
log.Fatalln(err)
}
iter.CurrentHash = block.PrevHash
return block
}

main.go 檔,新增一個 CommandLine 結構體和相關方法,讓使用者可以用命令列介面新增區塊到區塊鏈中,並且將區塊印出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
type CommandLine struct {
blockchain *blockchain.BlockChain
}

func (cli *CommandLine) printUsage() {
fmt.Println("Usage:")
fmt.Println(" add - adds the block to the chain")
fmt.Println(" print - prints the blocks in the chain")
}

func (cli *CommandLine) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
runtime.Goexit()
}
}

func (cli *CommandLine) addBlock(data string) {
cli.blockchain.AddBlock(data)
fmt.Println("Added Block!")
}

func (cli *CommandLine) printChain() {
iter := cli.blockchain.Iterator()
for {
block := iter.Next()
fmt.Printf("Previous Hash: %x\n", block.PrevHash)
fmt.Printf("Data in Block: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
pow := blockchain.NewProof(block)
fmt.Printf("Pow: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevHash) == 0 {
break
}
}
}

func (cli *CommandLine) run() {
cli.validateArgs()
addBlockCmd := flag.NewFlagSet("add", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("print", flag.ExitOnError)
addBlockData := addBlockCmd.String("block", "", "Block data")
switch os.Args[1] {
case "add":
err := addBlockCmd.Parse(os.Args[2:])
if err != nil {
log.Fatalln(err)
}
case "print":
err := printChainCmd.Parse(os.Args[2:])
if err != nil {
log.Fatalln(err)
}
default:
cli.printUsage()
runtime.Goexit()
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
runtime.Goexit()
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}

修改 main.go 檔,處理命令列介面的執行,並且在最後關閉資料庫連線。

1
2
3
4
5
6
7
8
func main() {
defer os.Exit(0)
chain := blockchain.InitBlockChain()
defer chain.Database.Close()

cli := CommandLine{chain}
cli.run()
}

完成後,可以使用 print 命令將區塊鏈印出來。

1
go run main.go print

由於資料庫中沒有區塊鏈,所以會建立一個創世區塊。

1
2
3
4
5
6
7
No existing blockchain found
00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Genesis proved
Previous Hash:
Data in Block: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Pow: true

使用 add -block 命令新增一個新的區塊。

1
go run main.go add -block "first block"

結果顯示如下。

1
2
00039d48149d795506c78f1e28f1fa0672ffd6b6cedfaaa4941f85d76e856e64
Added Block!

再將區塊鏈印出一次。

1
go run main.go print  

可以看到在創世區塊之後新增了一個新的區塊。

1
2
3
4
5
6
7
8
9
Previous Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Data in Block: first block
Hash: 00039d48149d795506c78f1e28f1fa0672ffd6b6cedfaaa4941f85d76e856e64
Pow: true

Previous Hash:
Data in Block: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Pow: true

程式碼

參考資料