블록체인/Mainnet

Go 언어로 블록체인 메인넷 만들기 - 작업증명(PoW)

이번엔 지난 포스트에서 만든 프로토타입 블록체인에 작업증명을 추가해보자. 지금은 블록을 아무런 작업 없이 생성할 수 있게 되어있으나, 비트코인에서는 작업증명 합의 알고리즘을 통해 블록을 생성한다. 퍼블릭 블록체인에서는 채굴자의 참여유도를 위해 블록 생성자에게 코인을 지급하게 되어있는데, 이것을 쉽게 해버리면 지급되는 코인의 가치가 사라지기 때문에 네트워크가 제대로 돌아가지 않을지도 있으므로 이러한 합의 알고리즘을 만드는 것은 중요하다고 할 수 있다.

 

여담으로 작업증명은 채굴(Mining)이라고도 할 정도로 작업시간이 오래걸릴 수도 있다. 이는 블록체인 내부에서 설정한 난이도에 따라 다르다. 이러한 난이도 설정도 해볼 것이다.

 

코드를 짜기전에 내용을 이해하는 것은 필수적이다. 작업증명은 단순히 말하자면 끝 자리가 0 으로 끝나는 비트의 수에 맞는 해시 값을 찾는 것이다. 이는 단순하지만 이것을 찾기 위해서는 단순히 해시값을 비교해보는 것 밖에는 방법이 없다. 먼저 I like donutsca07ca 라는 값을 SHA256 을 통해 돌려보면 다음과 같이 나타난다.

0x0000002f7c1fe31cb82acdc082cfec47620b7e4ab94f2bf9e096c436fc8cee06

위의 수치는 끝자리에 0이 6개다. 이는 16진수이므로 이는 24 bit 에 해당한다, 즉 난이도는 24 로 설정되어 있다는 것을 말한다. 또한 이는 다음의 값보다 작으며 이 값을 target 으로 정한다. 

0x0000010000000000000000000000000000000000000000000000000000000

하나의 사이클에 대해 target 보다 작은 해시 값을 찾으면 블록의 생성이 완료되고, 그렇지 않으면 반복한다. 이러한 target 을 지정하는 방법과 target 보다 작은 해시 값을 계산하는 과정은 글로 그대로 적는 것보다 코드를 보면서 이해하는 것이 더 좋을 것이다.

type ProofOfWork

ProofOfWork 타입은 해시값을 찾을 블록과 target 값을 가진다. target 값에 대해서는 다음의 함수에서 정할 것이다.

type ProofOfWork struct {
	block  *Block
	target *big.Int
}

func NewProofOfWork(*block) *ProofOfWork

작업증명을 생성하되, 여기서 중요한 것은 target 을 지정하는 일이다. 위와 같은 타겟은 어떻게 지정하면 좋을까? 그에 대한 아이디어는 Shift 연산자에 있다. target = 1 << 256-targetBits 에 해당하는 연산을 적용하게 되면 위와 같은 타겟을 만들 수 있는데, 숫자가 크기때문에 big.Int 를 사용할 것이다.

const targetBits = 24

func NewProofOfWork(b *Block) *ProofOfWork {
	target := big.NewInt(1)
	target.Lsh(target, 256-targetBits)

	return &ProofOfWork{b, target}
}

256 은 어디서 나온 것인가? 그것은 바로 블록의 해시 값을 만들기 위한 해시함수 SHA256 알고리즘의 결과값이 256 bit 에 해당하는 것에서 나온 것이다. 

func .prepareData(int64)

Nonce 라는 것은 단순 Counter 이며 이는 블록 헤더와 결합되어 target 보다 더 작은 값을 찾기위해 데이터를 준비시키기 위한 메서드라고 볼 수 있다. 기존의 블록 헤더에 있던 값들과 난이도를 포함하여 nonce 를 결합한 데이터를 준비한다.

func (pow *ProofOfWork) prepareData(nonce int64) []byte {
	data := bytes.Join([][]byte{
		pow.block.PrevBlockHash,
		pow.block.Data,
		IntToHex(pow.block.Timestamp),
		IntToHex(nonce),
		IntToHex(targetBits),
	}, []byte{})

	return data
}

func .Run() (int64, []byte)

작업증명을 진행하기 위한 메서드다. target.prepareData() 를 통해 준비한 데이터를 해시화 한 값과 비교하여 더 작으면 빠져나가고 그렇지 않으면 계속 진행한다.

func (pow *ProofOfWork) Run() (int64, []byte) {
	var nonce int64

	var hashInt big.Int
	var hash [32]byte

	for nonce < math.MaxInt64 {
		data := pow.prepareData(nonce)
		hash = sha256.Sum256(data)

		hashInt.SetBytes(hash[:])
		if hashInt.Cmp(pow.target) == -1 {
			break
		}
		nonce++
	}

	return nonce, hash[:]
}

func .Validate(*block) bool

블록이 작업증명을 통해 나온 것인지 증명하기 위한 것이며 블록에 있는 Nonce 값을 사용하여 한 번의 사이클로 증명할 수 있다.

func (pow *ProofOfWork) Validate(b *Block) bool {
	var hashInt big.Int
	data := pow.prepareData(b.Nonce)
	hash := sha256.Sum256(data)

	hashInt.SetBytes(hash[:])

	return hashInt.Cmp(pow.target) == -1
}

변경 사항

작업증명을 만들었으므로 기존의 작성한 코드에서 몇 가지 수정사항이 있으니 그것들을 살펴보도록 하자.

type Block

먼저 Block 타입에 Nonce 가 추가되어야 한다. Nonce 는 이 블록을 작업증명으로 채굴했다는 증명이다.

type Block struct {
	// ...
	Nonce         int64
}

func NewBlock(string, []byte) *Block

이제 블록을 생성할 때는 무조건 작업증명을 거쳐야 한다. 또한 작업증명 결과로 나온 것들에 대해서는 블록에 적용시켜야 할 필요가 있다.

func NewBlock(data string, prevBlockHash []byte) *Block {
	block := &Block{prevBlockHash, []byte{}, time.Now().Unix(), []byte(data), 0}
	pow := NewProofOfWork(block)
	block.Nonce, block.Hash = pow.Run()

	return block
}

func main()

작업증명을 구현한 것을 포함하여 출력을 살펴보자. 블록의 해시 값이 어떤 형태로 되어있는지 주목하자.

package main

import "fmt"

func main() {
	// ...
    
	for _, b := range bc.blocks {
		// ...
        
		pow := NewProofOfWork(b)
		fmt.Println("pow:", pow.Validate(b))

		fmt.Println()
	}
}
PrevBlockHash: 
Hash: 000000a5597f71205d53aee3177b6a7b72963cdd0a1abae12520260eb03481fa
Data: Genesis Block
pow: true

PrevBlockHash: 000000a5597f71205d53aee3177b6a7b72963cdd0a1abae12520260eb03481fa
Hash: 000000247e355eb6f830604018947a25dbe310cd027e31506e1fd564c5acdc49
Data: Send 1 BTC to Ivan
pow: true

PrevBlockHash: 000000247e355eb6f830604018947a25dbe310cd027e31506e1fd564c5acdc49
Hash: 000000cacdc15b2637e1caada1ff86074fd2a6712c0a40ecc1f043f2580f283b
Data: Send 2 more BTC to Ivan
pow: true