package blockchain
import (
"fmt"
"github.com/dgraph-io/badger/v3"
)
const (
// database Path.
dbPath = "./tmp/blocks"
)
// BlockChain structure
// 마지막 해쉬를 저장하고 DB 포인터를 저장해서 블록을 관리.
type BlockChain struct {
LastHash []byte
Database *badger.DB
}
// BlockChainIterator : DB에 저장된 블록체인을 순회하기 위해 생성.
type BlockChainIterator struct {
// 현재 가리키고 있는 hash
CurrentHash []byte
Database *badger.DB
}
// InitBlockChain : Genesis 블록을 시작으로 하는 블록체인을 생성한다.
func InitBlockChain() *BlockChain {
var lastHash []byte
opts := badger.DefaultOptions(dbPath)
// store key and metadata
opts.Dir = dbPath
// store all values
opts.ValueDir = dbPath
db, err := badger.Open(opts)
Handle(err)
// Update : read and write transactions on our databases.
// Txn : Transaction, which can be read-only or read-write.
err = db.Update(func(txn *badger.Txn) error {
// Blockchain이 비어있다는 뜻. (KeyNotFound)
// lh(last hash) key 가 없음.
if _, err := txn.Get([]byte("lh")); err == badger.ErrKeyNotFound {
fmt.Println("No existing blockchain found")
// 검증된 Genesis Block을 생성.
genesis := Genesis()
fmt.Println("Genesis proved")
// transaction에 저장.
// genesis.Hash -> genesis.Seriaize()
err = txn.Set(genesis.Hash, genesis.Serialize())
Handle(err)
// lh -> genesis.Hash
err = txn.Set([]byte("lh"), genesis.Hash)
lastHash = genesis.Hash
return err
// lh key가 존재하는 경우.
} else {
// lh 키의 value를 lastHash에 할당.
item, err := txn.Get([]byte("lh"))
Handle(err)
lastHash, err = item.ValueCopy(nil)
return err
}
})
Handle(err)
blockchain := BlockChain{lastHash, db}
return &blockchain
}
// AddBlock : data의 값을 가지는 블록을 추가한다.
func (chain *BlockChain) AddBlock(data string) {
var lastHash []byte
// View : read-only type of transaction
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)
// 이전 해쉬값과 데이터로 새로운 블록을 생성.
newBlock := CreateBlock(data, lastHash)
err = chain.Database.Update(func(txn *badger.Txn) error {
// 새로운 블록의 해쉬값을 저장한다.
err := txn.Set(newBlock.Hash, newBlock.Serialize())
Handle(err)
// lh 키값을 새로운 블록의 해쉬값으로 바꾼다.
err = txn.Set([]byte("lh"), newBlock.Hash)
// BlockChain의 마지막 해쉬를 새로운 블록의 해쉬값으로 지정한다.
chain.LastHash = newBlock.Hash
return err
})
Handle(err)
}
// Iterator : 블록체인 이터레이터. LastHash부터 이전 해쉬로 가며 순회할 수 있다.
func (chain *BlockChain) Iterator() *BlockChainIterator {
iter := &BlockChainIterator{chain.LastHash, chain.Database}
return iter
}
// Next : BlockChain의 다음 블록을 반환한다.
func (iter *BlockChainIterator) Next() *Block {
var block *Block
err := iter.Database.View(func(txn *badger.Txn) error {
// 현재 가리키고 있는 hash를 deserialize해서 Block을 복원한다.
item, err := txn.Get(iter.CurrentHash)
Handle(err)
encodedBlock, err := item.ValueCopy(nil)
block = Deserialize(encodedBlock)
return err
})
Handle(err)
// iter가 이전 해시를 가리키도록 한다.
iter.CurrentHash = block.PrevHash
return block
}
package blockchain
import (
"bytes"
"encoding/gob"
"log"
)
// Block structure
type Block struct {
Hash []byte
Data []byte
PrevHash []byte
Nonce int
Difficulty int
}
// CreateBlock : data와 prevHash를 받아서 새로운 Hash를 생성한 블록을 생성한다.
// difficulty를 조절한다. 여기서는 그냥 고정값으로 넣었다.
func CreateBlock(data string, prevHash []byte) *Block {
difficulty := 12
block := &Block{[]byte{}, []byte(data), prevHash, 0, difficulty}
// PoW 조건에 맞는 블록을 생성한다.
pow := NewProof(block)
nonce, hash := pow.Run(difficulty)
block.Hash = hash[:]
block.Nonce = nonce
block.Difficulty = difficulty
return block
}
// Genesis : 체인의 맨 처음 블록이다. prevHash 값이 비어있다.
func Genesis() *Block {
return CreateBlock("Genesis", []byte{})
}
// Serialize : BadgerDB 에 값을 넣기 위해 byte배열로 바꿔준다.
func (b *Block) Serialize() []byte {
var res bytes.Buffer
encoder := gob.NewEncoder(&res)
err := encoder.Encode(b)
Handle(err)
return res.Bytes()
}
// Deserialize : data를 decoding 해서 Block객체로 바꿔준다.
func Deserialize(data []byte) *Block {
var block Block
decoder := gob.NewDecoder(bytes.NewReader(data))
err := decoder.Decode(&block)
Handle(err)
return &block
}
// Handle : Error handling.
func Handle(err error) {
if err != nil {
log.Panic(err)
}
}
package main
import (
"flag"
"fmt"
"os"
"runtime"
"strconv"
"github.com/HTaeha/Blockchain-in-Golang/blockchain"
)
// CommandLine : CommandLine으로 원하는 동작을 실행시킬 수 있도록 한다.
type CommandLine struct {
blockchain *blockchain.BlockChain
}
// printUsage : print cli의 사용법을 알려준다.
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")
}
// validateArgs : Argument를 검증한다.
func (cli *CommandLine) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
// runtime.Goexit은 현재 goroutine을 종료시킨다.
// main 프로그램은 정상적으로 돌기 때문에 DB가 충돌을 일으키지 않고 종료할 수 있다.
// os.exit()을 사용하면 프로그램 자체를 종료시키기 때문에 DB가 정상종료되지 않을 수 있다.
runtime.Goexit()
}
}
// addBlock : cli를 통해 블록을 추가한다.
func (cli *CommandLine) addBlock(data string) {
cli.blockchain.AddBlock(data)
fmt.Println("Added Block!")
}
// printChain : 블록체인에서 마지막부터 첫번째 블록 내용을 출력한다.
func (cli *CommandLine) printChain() {
iter := cli.blockchain.Iterator()
for {
block := iter.Next()
fmt.Printf("Previous Hash: %x\n", block.PrevHash)
fmt.Printf("Data: %s\n", block.Data)
fmt.Printf("Hash: %x\n", block.Hash)
// Validate 과정은 매우 빠르게 처리된다.
pow := blockchain.NewProof(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
fmt.Println()
if len(block.PrevHash) == 0 {
break
}
}
}
// run Command Line Interface.
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] {
// add command 일 때 파싱
case "add":
err := addBlockCmd.Parse(os.Args[2:])
blockchain.Handle(err)
// print command 일 때 파싱
case "print":
err := printChainCmd.Parse(os.Args[2:])
blockchain.Handle(err)
default:
cli.printUsage()
runtime.Goexit()
}
if addBlockCmd.Parsed() {
// addBlockData가 없으면 Usage() 실행 후 종료.
if *addBlockData == "" {
addBlockCmd.Usage()
runtime.Goexit()
}
// 블록 추가.
cli.addBlock(*addBlockData)
}
// print command 일 때 printChain()을 실행.
if printChainCmd.Parsed() {
cli.printChain()
}
}
func main() {
defer os.Exit(0)
chain := blockchain.InitBlockChain()
// 메인이 종료되기 전에 DB를 종료.
defer chain.Database.Close()
cli := CommandLine{chain}
cli.run()
}
package blockchain
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"log"
"math"
"math/big"
)
// Take the data from the block
// create a counter (nonce) which starts at 0
// create a hash of the data plus the counter
// check the hash to see if it meets a set of requirements
// Requirements:
// The First few bytes must contain 0s
// Difficulty : 채굴하기 위한 문제의 난이도.
// 256bit 중에 Difficulty만큼의 0을 찾는다.
// const Difficulty = 12
// ProofOfWork structure
type ProofOfWork struct {
Block *Block
// 정답. Target 보다 작은 값을 찾으면 정답이다.
Target *big.Int
}
// NewProof : ProofOfWork 객체를 만들어 리턴한다.
func NewProof(b *Block) *ProofOfWork {
target := big.NewInt(1)
// Left shift : 아래 식의 결과는 2^(256-Difficulty)가 된다.
// target은 전체 256비트 중에 왼쪽에 Difficulty-1 만큼의 0이 존재한다. (2진수)
// 아래 값보다 작은 값이면 0이 Difficulty만큼 존재하는 것이기 때문에 정답이다.
target.Lsh(target, uint(256-b.Difficulty))
pow := &ProofOfWork{b, target}
return pow
}
// InitData : PrevHash, Data, nonce, Difficulty 를 합쳐 데이터를 만든다.
func (pow *ProofOfWork) InitData(nonce, Difficulty int) []byte {
data := bytes.Join(
[][]byte{
pow.Block.PrevHash,
pow.Block.Data,
ToHex(int64(nonce)),
ToHex(int64(Difficulty)),
},
[]byte{},
)
return data
}
// Run : 정답을 찾아 nonce, hash 값을 반환한다.
// difficulty의 값이 클수록 난이도가 어려워진다. (run이 오래 걸린다.)
func (pow *ProofOfWork) Run(difficulty int) (int, []byte) {
var intHash big.Int
var hash [32]byte
nonce := 0
// MaxInt64 == 2^63 - 1
for nonce < math.MaxInt64 {
data := pow.InitData(nonce, difficulty)
hash = sha256.Sum256(data)
fmt.Printf("\r%x", hash)
intHash.SetBytes(hash[:])
// intHash가 pow.Target보다 작은 값이면 정답.
if intHash.Cmp(pow.Target) == -1 {
break
} else {
nonce++
}
}
fmt.Println()
return nonce, hash[:]
}
// Validate : PoW 가 유효한지 검증한다. 매우 간단하게 처리 가능하다.
// Run 할 때보다 매우 쉽고 빠르게 처리된다.
func (pow *ProofOfWork) Validate() bool {
var intHash big.Int
// Block의 Nonce값을 이용해 hash 값을 재현한다.
data := pow.InitData(pow.Block.Nonce, pow.Block.Difficulty)
hash := sha256.Sum256(data)
intHash.SetBytes(hash[:])
return intHash.Cmp(pow.Target) == -1
}
// ToHex : int64를 []byte로 변환.
func ToHex(num int64) []byte {
buff := new(bytes.Buffer)
err := binary.Write(buff, binary.BigEndian, num)
if err != nil {
log.Panic(err)
}
return buff.Bytes()
}
// 블록을 7개 생성한 후 출력한 모습이다.
// Difficulty를 바꿔가며 블록을 생성했지만 모두 제대로 검증된 것을 볼 수 있다.
Previous Hash: 00000ad03df10d90dd9e9d80d7edf298766d4e494b94f7ee47626671a71aaa6c
Data: test12
Hash: 0002da7f56f3a390f83119d364fe204b4245dbd90a376e4d960908747f703280
PoW: true
Previous Hash: 00030c0beb186d5d886973a80a5f64ab1bc554bea9acb478d7f389728716545a
Data: test18
Hash: 00000ad03df10d90dd9e9d80d7edf298766d4e494b94f7ee47626671a71aaa6c
PoW: true
Previous Hash: 0000037e524f932bb1cb743fc0298d32fbbf81b7fcee7e7eacbb3f31ce1da300
Data: test
Hash: 00030c0beb186d5d886973a80a5f64ab1bc554bea9acb478d7f389728716545a
PoW: true
Previous Hash: 0005a472e7f14826f3e1241b8fda875916441d9ad292a2939f6c0b5850cf71a9
Data: difficulty18
Hash: 0000037e524f932bb1cb743fc0298d32fbbf81b7fcee7e7eacbb3f31ce1da300
PoW: true
Previous Hash: 000f75abfa51468e78270ff9ba19ee95cfd2eb94ca5b9a6f41a90c170076b663
Data: taeha's block
Hash: 0005a472e7f14826f3e1241b8fda875916441d9ad292a2939f6c0b5850cf71a9
PoW: true
Previous Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Data: firstblock
Hash: 000f75abfa51468e78270ff9ba19ee95cfd2eb94ca5b9a6f41a90c170076b663
PoW: true
Previous Hash:
Data: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
PoW: true