#3 Save blockchain persistently
블록체인을 DB에 저장하자.
BadgerDB
blockchain/blockchain.go

blockchain/block.go
main.go
Go run main.go
Result
Last updated
블록체인을 DB에 저장하자.

Last updated
package blockchain
import (
"fmt"
"github.com/dgraph-io/badger"
)
const (
dbPath = "./tmp/blocks"
)
// Badger DB를 고려해서 BlockChain을 재설계
// 기존의 Block slice는 메모리에 상주하기 때문에 프로그램 종료시 없어짐.
// DB를 가르키는 포인터를 저장해서 포인터를 통해 블록 관리
type BlockChain struct {
LastHash []byte // 마지막 블록의 hash
Database *badger.DB // Badger DB를 가르키는 포인터
}
// BlockChain DB의 Block을 순회하는 자료구조
type BlockChainIterator struct {
CurrentHash []byte
Database *badger.DB
}
// Blockchain이 DB에 저장되어 있다면 불러오고,
// 없다면 새로 만들어 반환하는 함수
func InitBlockChain() *BlockChain {
var lastHash []byte
// File명을 통해 DB를 엽니다.
db, err := badger.Open(badger.DefaultOptions(dbPath))
Handle(err)
// db.Update는 Read/Write함수, View는 Read Only 함수입니다.
// 수정사항(Genesis 생성)이 있기 때문에 Update함수를 사용합니다.
err = db.Update(func(txn *badger.Txn) error {
// Txn(transaction) closure
// "lh"(lash hash)로 검색했는데 키가 발견되지 않았다면 저장이 안되어 있는것.
if _, err := txn.Get([]byte("lh")); err == badger.ErrKeyNotFound {
fmt.Println("No existing blockchain found")
genesis := Genesis()
fmt.Println("Genesis proved")
// Key{genesis.Hash}, Value{genesis.Serialize()}를 넣습니다.
// Serialize()함수로 block을 []byte로 바꾸어 저장합니다.
err = txn.Set(genesis.Hash, genesis.Serialize())
Handle(err)
// "lh" (마지막 해시)도 저장합니다.
err = txn.Set([]byte("lh"), genesis.Hash)
lastHash = genesis.Hash
return err
} else {
// "lh"가 검색이되면 블록체인이 저장되어 있는 것.
item, err := txn.Get([]byte("lh"))
Handle(err)
lastHash, err = item.ValueCopy(nil)
return err
}
})
Handle(err)
// 마지막 해시와 db pointer를 인자로하여 블록체인을 생성합니다.
blockchain := BlockChain{lastHash, db}
return &blockchain
}
// 새로운 블록을 만들어서 블록체인에 연결하는 함수
func (chain *BlockChain) AddBlock(data string) {
var lastHash []byte
// Read만 하므로 View를 사용
err := chain.Database.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte("lh"))
Handle(err)
lastHash, err = item.ValueCopy(nil)
return err
})
Handle(err)
// lashHash를 토대로 다음 문제를 풀어 새로운 블록을 생성.
newBlock := CreateBlock(data, lastHash)
// 블록의 해시를 키값으로 새로운 블록을 저장하고
// lh의 값 또한 새로운 블록의 해시로 업데이트 해줍니다.
err = chain.Database.Update(func(txn *badger.Txn) error {
err := txn.Set(newBlock.Hash, newBlock.Serialize())
Handle(err)
err = txn.Set([]byte("lh"), newBlock.Hash)
chain.LastHash = newBlock.Hash
return err
})
Handle(err)
}
// 아래 함수들은 Block을 iteration할 수 있도록 도와주는
// Iterator 관련 함수입니다.
// 아래 함수는 BlockChainIterator를 생성하여 반환합니다.
func (chain *BlockChain) Iterator() *BlockChainIterator {
iter := &BlockChainIterator{chain.LastHash, chain.Database}
return iter
}
// Iterator는 순회를 목적으로 하기때문에
// 다음 객체를 반환하는 것이 중요합니다.
// Next()함수는 최신 블록에서 Genesis블록 쪽으로
// 다음 블록을 탐색해 포인터를 반환합니다.
func (iter *BlockChainIterator) Next() *Block {
var block *Block
// 현재 해시값 {CurrentHash}로 블록을 검색합니다.
err := iter.Database.View(func(txn *badger.Txn) error {
item, err := txn.Get(iter.CurrentHash)
Handle(err)
encodedBlock, err := item.ValueCopy(nil)
block = Deserialize(encodedBlock)
return err
})
Handle(err)
// block에 저장된 PrevHash를 가져와서
// 다음 탐색에 사용합니다.
iter.CurrentHash = block.PrevHash
return block
}// Badger DB가 arrays of byte 밖에 수용하지 못하기 때문에
// Block data structure를 serialize, deserialize해줄
// Util함수가 필요하다.
func (b *Block) Serialize() []byte {
var res bytes.Buffer
encoder := gob.NewEncoder(&res)
err := encoder.Encode(b)
Handle(err)
return res.Bytes()
}
func Deserialize(data []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(data))
err := decoder.Decode(&block)
Handle(err)
return &block
}
func Handle(err error) {
if err != nil {
log.Panic(err)
}
}// main.go
package main
import (
"flag"
"fmt"
"os"
"runtime"
"strconv"
"github.com/siisee11/golang-blockchain/blockchain"
)
// CommandLine은 BlockChain과 상호작용을 해야합니다.
type CommandLine struct {
blockchain *blockchain.BlockChain
}
// Cli help 메세지 입니다.
func (cli *CommandLine) printUsage() {
fmt.Println("Usage: ")
fmt.Println(" add -block BLOCK_DATA - Add a block to the chain ")
fmt.Println(" print - Prints the blocks in the chain")
}
// Args(arguments)가 1개면 명령어를 입력하지 않은 것이므로 종료합니다.
func (cli *CommandLine) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
// runtime.Goexit은 Go routine을 종료시키는 것이기 때문에
// applicaion 강제 종료가 아니여서 DB가 정상 종료(close)될 수 있도록 해준다.
runtime.Goexit()
}
}
// AddBlock을 데이터를 담아 호출하여 새로운 블록을 만듭니다.
func (cli *CommandLine) addBlock(data string) {
cli.blockchain.AddBlock(data)
fmt.Println("Added Block!")
}
// Chain을 순회하며 블록을 출력합니다.
// LastHash 부터 Genesis순으로 출력합니다. (Iterator 구현을 기억!)
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 Genesis
if len(block.PrevHash) == 0 {
break
}
}
}
func (cli *CommandLine) run() {
cli.validateArgs()
// Go의 option 처리하는 함수들.
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:])
blockchain.Handle(err)
case "print":
err := printChainCmd.Parse(os.Args[2:])
blockchain.Handle(err)
default:
cli.printUsage()
runtime.Goexit()
}
if addBlockCmd.Parsed() {
if *addBlockData == "" {
addBlockCmd.Usage()
runtime.Goexit()
}
cli.addBlock(*addBlockData)
}
if printChainCmd.Parsed() {
cli.printChain()
}
}
func main() {
defer os.Exit(0)
// Blockchain을 초기화 한다. 이는 Genesis block을 만드는 작업을 포함한다.
chain := blockchain.InitBlockChain()
defer chain.Database.Close()
cli := CommandLine{chain}
cli.run()
}mkdir tmp/
go run main.go printgo get github.com/dgraph-io/badger/v3
go mod tidygo run main.go print
badger 2021/04/27 00:49:17 DEBUG: Value log discard stats empty
Previous Hash:
Data in Block: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
PoW: truego run main.go add -block "first blocks"
badger 2021/04/27 00:51:41 INFO: All 1 tables opened in 3ms
badger 2021/04/27 00:51:41 INFO: Replaying file id: 0 at offset: 280
badger 2021/04/27 00:51:41 INFO: Replay took: 3.792µs
badger 2021/04/27 00:51:41 DEBUG: Value log discard stats empty
00002cfd26b0fad5ed42c55d33aae5046cf3d0e822c3b9c159462cf2a1905eba
Added Block!Previous Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Data in Block: first blocks
Hash: 00002cfd26b0fad5ed42c55d33aae5046cf3d0e822c3b9c159462cf2a1905eba
PoW: true
Previous Hash:
Data in Block: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
PoW: false