前言 本文為「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 { 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) } err = txn.Set([]byte ("lh" ), genesis.Hash) lastHash = genesis.Hash return err } 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 { 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) } 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 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 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
程式碼
參考資料