2. Proof of work
PoW에 대해 공부하고 구현해보자.
Proof of work
block이 채굴되는 시간을 일정하게 하기 위한 방법.
miner가 많아지면 difficulty를 올린다. (채굴되는 시간을 일정하게 하기 위해)
처음보는 메소드 정리
big.NewInt()
큰 수를 생성한다.
최대 값이 명시되어 있지 않고 이론적으로 메모리 사이즈 또는 최대 배열 크기만큼 사용할 수 있다고 한다.
byte
uint8 과 동일.
bytes.Join()
func Join(s [][]byte, sep []byte) []byte
math.MaxInt64
1<<63 - 1 과 같은 값. 2^63 - 1
sha256.Sum256()
Sum256 returns the SHA256 checksum of the data.
proof.go
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-Difficulty))
pow := &ProofOfWork{b, target}
return pow
}
// InitData : PrevHash, Data, nonce, Difficulty 를 합쳐 데이터를 만든다.
// 이 부분에서 Difficulty를 같이 넣어서 data를 만드는 것이 이해되지 않는다. Difficulty를 빼도 영향이 없지 않을까?
func (pow *ProofOfWork) InitData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.Block.PrevHash,
pow.Block.Data,
ToHex(int64(nonce)),
ToHex(int64(Difficulty)),
},
[]byte{},
)
return data
}
// Run : 정답을 찾아 nonce, hash 값을 반환한다.
func (pow *ProofOfWork) Run() (int, []byte) {
var intHash big.Int
var hash [32]byte
nonce := 0
// MaxInt64 == 2^63 - 1
for nonce < math.MaxInt64 {
data := pow.InitData(nonce)
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)
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()
}
block.go
package blockchain
// BlockChain structure
type BlockChain struct {
Blocks []*Block
}
// Block structure
type Block struct {
Hash []byte
Data []byte
PrevHash []byte
Nonce int
}
// CreateBlock : data와 prevHash를 받아서 새로운 Hash를 생성한 블록을 생성한다.
func CreateBlock(data string, prevHash []byte) *Block {
block := &Block{[]byte{}, []byte(data), prevHash, 0}
// PoW 조건에 맞는 블록을 생성한다.
pow := NewProof(block)
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}
// AddBlock : data의 값을 가지는 블록을 추가한다.
func (chain *BlockChain) AddBlock(data string) {
prevBlock := chain.Blocks[len(chain.Blocks)-1]
new := CreateBlock(data, prevBlock.Hash)
chain.Blocks = append(chain.Blocks, new)
}
// Genesis : 체인의 맨 처음 블록이다. prevHash 값이 비어있다.
func Genesis() *Block {
return CreateBlock("Genesis", []byte{})
}
// InitBlockChain : Genesis 블록을 시작으로 하는 블록체인을 생성한다.
func InitBlockChain() *BlockChain {
return &BlockChain{[]*Block{Genesis()}}
}
main.go
package main
import (
"Blockchain-in-Golang/blockchain"
"fmt"
"strconv"
)
func main() {
// chain := InitBlockChain()
chain := blockchain.InitBlockChain()
chain.AddBlock("First Block after Genesis")
chain.AddBlock("Second Block after Genesis")
chain.AddBlock("Third Block after Genesis")
for _, block := range chain.Blocks {
fmt.Printf("Previous Hash: %x\n", block.PrevHash)
fmt.Printf("Data in Block: %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()
}
}
Result
// 생성된 4개 블록의 Target(정답)들이다.
// Difficulty를 12로 설정했기 때문에 맨 왼쪽 12개의 0이 있다.
// 16진수로 0이 3개 이므로 2진수로 12개.
00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
0004458722d47515269d8ddbe22e2a2b5a260bd9359a3b7d72a9888b14f9f5f5
000589525b1a774b7d1ffcbf471d32eccea3a8f826c463dffdf09a2261c0be12
000832555d05d9951dd3bfc6c1c4c511807c7d68a01ac4f91adce549eacfa00c
Previous Hash:
Data in Block: Genesis
Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
PoW: true
Previous Hash: 00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
Data in Block: First Block after Genesis
Hash: 0004458722d47515269d8ddbe22e2a2b5a260bd9359a3b7d72a9888b14f9f5f5
PoW: true
Previous Hash: 0004458722d47515269d8ddbe22e2a2b5a260bd9359a3b7d72a9888b14f9f5f5
Data in Block: Second Block after Genesis
Hash: 000589525b1a774b7d1ffcbf471d32eccea3a8f826c463dffdf09a2261c0be12
PoW: true
Previous Hash: 000589525b1a774b7d1ffcbf471d32eccea3a8f826c463dffdf09a2261c0be12
Data in Block: Third Block after Genesis
Hash: 000832555d05d9951dd3bfc6c1c4c511807c7d68a01ac4f91adce549eacfa00c
PoW: true
Question
proof.go의 InitData()에서 데이터르 생성할 때 Difficulty를 포함시키는 이유
Difficulty가 없더라도 문제 없을 것 같다.
Answer : Difficulty가 변함. 그에 따라 데이터도 변해야 함. nonce처럼 블록 내부와 validate 과정에 difficulty를 추가해야 됨. 하지만 지금 코드에서는 반영 되어 있지 않음.
Code
아래 링크에서 전체 코드를 찾아볼 수 있다.
Last updated