#2 Proof-of-Work

Blockchain의 꽃, PoW를 벌써 코딩해봅시다.

blockchain/proof.go

이전 코드에서 AddBlock함수를 부르면 무한으로 블록을 추가할 수 있었습니다. 이제 작업증명 방식을 도입해서 AddBlock함수가 불렸을 때 작업을 진행하고 작업이 완료되어야 블록을 추가할 수 있도록 바꾸겠습니다. 이 부분에 해당하는 이론적 지식은 blockchain structure 혹은 PoW 에서 찾아볼 수 있습니다.

blockchain/proof.go 파일을 생성하고 아래 내용을 붙혀넣습니다. 자세한 내용은 주석을 참고하세요.

package blockchain

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"log"
	"math"
	"math/big"
)

// 우리가 찾고자하는 정답을 정의하겠습니다.
// 우리는 256bit중 왼쪽 {Difficulty}만큼의 bit가 0인 답을 원합니다.
const Difficulty = 12

type ProofOfWork struct {
	Block  *Block
	Target *big.Int // 문제의 답 (Difficulty로 부터 얻어낸다.)

	// big.Int에 관한 내용 참고 https://golang.org/pkg/math/big/
}

// Block을 받아서 ProofOfWork struct를 반환한다.
// Difficulty로 Target을 만든다.
func NewProof(b *Block) *ProofOfWork {
	target := big.NewInt(1)
	// 1을 왼쪽으로 256-Difficulty 만큼 이동시킨다.
	// PoW에서 target보다 작은 수가 나오는 것을 정답으로 할 것이다.
	// ** 작다는 뜻을 잘 생각해보자 왼쪽에 0이 Difficulty만큼 나오는 것과 같은 뜻이다.
	target.Lsh(target, uint(256-Difficulty))

	pow := &ProofOfWork{b, target}

	return pow
}

// Block의 데이터, 이전 해시, nonce, Difficulty 값을 모두 합쳐서 데이터를 만든다.
// 이 데이터를 sha256한 값이 정답이라면 이 데이터가 적힌 블록이 추가된다.
func (pow *ProofOfWork) InitData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.Block.PrevHash,
			pow.Block.Data,
			ToHex(int64(nonce)),
			ToHex(int64(Difficulty)),
		},
		[]byte{}, // seperator
	)
	return data
}

// PoW를 계산하여 nonce와 정답 hash값을 반환하는 함수
func (pow *ProofOfWork) Run() (int, []byte) {
	var intHash big.Int
	var hash [32]byte
	nonce := 0

	// 사실상 무한루프
	for nonce < math.MaxInt64 {
		// nonce를 포함하여 계산된 데이터를 가져온다.
		data := pow.InitData(nonce)
		// 데이터의 해시값.
		hash = sha256.Sum256(data)

		fmt.Printf("\r%x", hash)
		// 해시값으로 big.Int만듬
		intHash.SetBytes(hash[:])

		if intHash.Cmp(pow.Target) == -1 {
			// Target보다 intHash가 작다는 뜻, 즉 정답.
			break
		} else {
			// 다음 논스를 시도하자
			nonce++
		}
	}
	fmt.Println()

	// nonce와 결과 hash값을 반환
	return nonce, hash[:]
}

// 정답이 맞는지 검사하는 과정이다.
// Run에 비해 얼마나 쉬운지 알 수 있다. (단방향성)
func (pow *ProofOfWork) Validate() bool {
	var intHash big.Int
	// 블록에 포함된 Nonce를 통해 데이터 재현
	data := pow.InitData(pow.Block.Nonce)
	hash := sha256.Sum256(data)
	intHash.SetBytes(hash[:])

	return intHash.Cmp(pow.Target) == -1
}

// int64를 받아서 바이트로 변환하는 유틸리티 함수
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()
}

blockchain/block.go

블록을 만드는 방식을 바꿔야합니다. 블록에 Nonce가 추가되었고, deriveBlock이 아닌 작업 증명 방식으로 Block을 생성하도록 코드를 수정합니다.

// blockchain/block.go
package blockchain

type BlockChain struct {
	// BlockChain은 Block포인터 슬라이스를 가진다.
	Blocks []*Block
}

// Block의 구조
type Block struct {
	Hash     []byte // 현재 블록의 해시
	Data     []byte // 블록에 기록된 data
	PrevHash []byte // 이전 블록의 해시
	Nonce    int
}

// Block을 생성하는 함수
// data와 이전 해시값을 인자로 받는다.
func CreateBlock(data string, prevHash []byte) *Block {
	// data와 이전 해시값으로 block을 만들고
	block := &Block{[]byte{}, []byte(data), prevHash, 0}
	pow := NewProof(block)
	nonce, hash := pow.Run()

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

// 새로운 블록을 만들어서 블록체인에 연결하는 함수
func (chain *BlockChain) AddBlock(data string) {
	prevBlock := chain.Blocks[len(chain.Blocks)-1]
	new := CreateBlock(data, prevBlock.Hash)
	chain.Blocks = append(chain.Blocks, new)
}

// Chain의 첫 블록을 Genesis Block이라고 한다.
// Genesis Block은 이전 해시가 없으므로 예외처리한다.
func Genesis() *Block {
	return CreateBlock("Genesis", []byte{})
}

func InitBlockChain() *BlockChain {
	return &BlockChain{[]*Block{Genesis()}}
}

main.go

main을 수정해서 PoW가 잘 동작하는지 테스트합니다. 미리 만들어진 블록을 받아와서 Validate를 진행해봅니다.

// main.go
package main

import (
	"fmt"
	"strconv"

	"github.com/siisee11/golang-blockchain/blockchain"
)

func main() {
	// Blockchain을 초기화 한다. 이는 Genesis block을 만드는 작업을 포함한다.
	chain := blockchain.InitBlockChain()

	// 예시로 3개의 블록을 추가한다.
	chain.AddBlock("First Block after Genesis")
	chain.AddBlock("second Block after Genesis")
	chain.AddBlock("Third Block after Genesis")

	// Block을 iterate하며 출력한다.
	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)

		pow := blockchain.NewProof(block)
		fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
		fmt.Println()
	}
}

메인을 실행시키면 아래와 같은 결과물이 나옵니다. //로 시작하는 부분은 추가로 주석 설명한 부분입니다.

// 아래 4줄은 정답으로 찾아진 hash값들 입니다. (proof.go:66)
// Difficulty가 12이기 때문에 12bit => 000000000000(bit) = 0x000
// 즉, 맨 앞 3수가 0이다.

00031a02a972efd4fa6ea999407149b85b03ccecb8c2bb8eb5a1d068862309d0
0004458722d47515269d8ddbe22e2a2b5a260bd9359a3b7d72a9888b14f9f5f5
000765e0734dd46470c85341087e5a5203e80109df4ae0185d6cd9aa04dba4bd
0008a5c131ccd53c60db5797f3513556b6f2ce22df2a07482e0120f3dc2a5953

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: 000765e0734dd46470c85341087e5a5203e80109df4ae0185d6cd9aa04dba4bd
PoW: true

Previous Hash: 000765e0734dd46470c85341087e5a5203e80109df4ae0185d6cd9aa04dba4bd
Data in Block: Third Block after Genesis
Hash: 0008a5c131ccd53c60db5797f3513556b6f2ce22df2a07482e0120f3dc2a5953
PoW: true

Difficulty를 18로 바꿔서 돌려보자.

블록 추가는 어려운데, 검증은 쉬운것을 바로 체감할 수 있다.

코드는 https://github.com/siisee11/golang-blockchain 의 step2 브랜치에 있습니다 .

Last update: 04/26/2021

Last updated