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

  1. proof.go의 InitData()에서 데이터르 생성할 때 Difficulty를 포함시키는 이유

    • Difficulty가 없더라도 문제 없을 것 같다.

    • Answer : Difficulty가 변함. 그에 따라 데이터도 변해야 함. nonce처럼 블록 내부와 validate 과정에 difficulty를 추가해야 됨. 하지만 지금 코드에서는 반영 되어 있지 않음.

Code

아래 링크에서 전체 코드를 찾아볼 수 있다.

Last updated