참고 링크 : https://jeiwan.net
블록체인에 대해 공부를 해보고 알고 있는 사람이라면 합의 알고리즘에 대해서 들어보았을 것 입니다.
합의 알고리즘은 일종의 법칙입니다.
미국법, 대한민국법 이와 같이 나라같이 정해진 법이 있고
플랫폼마다 원하는 형태로 이러한 형식을 따르고 있습니다.
대표적으로는 PoW, PoS이런 합의 알고리즘이 있으며 저희는 이번에 PoW에 대해서 다루어 보겠습니다.
원래 PoW라고 하면 변화하는 값이 필요하지만 저희는 그런 부분까지는 고려하지 않았기 떄문에 단순히 상수값을 통해서 작성을 해보았습니다.
const targetBits = 24
블록의 난이도를 의미할 변수 값 입니다.
이후 작업증명을 담당할 구조체를 선언해 줍니다.
type ProofOfWork struct {
block *Block
target *big.Int
//작업 증명을 담당할 구조체를 선언
}
여기에서 target일종의 난이도를 의미하며 해시값을 큰 정수로 변환함으로써 해당 값이 target이라는 값보다 작은지를 검증하면서 알고리즘이 작동하게 될 것입니다.
이후 해당 해시값을 계산해 주는 함수가 필요 합니다.
func NewProofOwWork(b *Block) *ProofOfWork {
target := big.NewInt(1)
target.Lsh(target, uint(256-targetBits))
pow := &ProofOfWork{b, target}
return pow
}
안써본 패키지가 많아서 천천히 알아 보았습니다.
일단 기본적으로 target을 big.NewInt()를 통해서 초기화 해줍니다.
그후 시프트 연산을 시전합니다.
시프트 연산은 상당히 복잡한 연산입니다.
만약 1 << 2 와 같은 시프트 연산을 실행한다면
0001 이라는 비트값이 --> 0100이 되는 것과 같습니다.
즉 1이라는 값이 4가 되는 효과를 나타냅니다.
- 대략 2의n배의 값을 가지게 됩니다.
코드에 보이는 대로 시프트 연산을 실시하면 아마 굉장히 큰 숫자가 나오겠죠
이렇게 나온 값을 저희는 맞춰야하는 값으로 인식을 하고 앞으로 이러한 값보다 작은 값이 들어오게 되면 트랜잭션이 성공적으로 검증이 되는 것을 의미하게 될 것입니다.
이 부분에 대해서는 굉장히 복잡해서 이해를 하는데에 많은 시간이 걸렸습니다.
또한 int값을 hex값으로 변화하고 해당 변환값이 bytes이어야 하는 부분에서 많은 고민을 했던것 같습니다.
func IntToHex(num int64) []byte {
buff := new(bytes.Buffer)
binary.Write(buff, binary.BigEndian, num)
return buff.Bytes()
}
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join([][]byte{
pow.block.PrevBlockHash,
pow.block.Data,
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{})
return data
}
일단 기본적으로 IntToHex
에 대해서 알아야 합니다.
golang의 pkg사이트를 참고해보면 new(bytes.Buffer)
이라는 함수는 bytes를 가지고 있는 슬라이스를 만들어 주는 함수 입니다.
이후 Write를 통해서 bytes슬라이스를 채워줍니다.
Write writes the binary representation of data into w. Data must be a fixed-size value or a slice of fixed-size values, or a pointer to such data. Boolean values encode as one byte: 1 for true, and 0 for false. Bytes written to w are encoded using the specified byte order and read from successive fields of the data. When writing structs, zero values are written for fields with blank (_) field names
네 저는 영어를 잘 모르지만 간단하게 알아듣게 설명을 하자면
W에 값을 입력합니다. Data는 반드시 고정값또는 고정된 사이즈를 가진 슬라이여야 합니다.
-여기에서 Data부분은 맨끝 인자를 말합니다.
에러를 한개 받을수 있고 W에 에 기록된 bytes는 특병한 방법으로 인코딩 되며 성공적인 데이터 필드에서 읽어집니다??
그후 prepareData라는 함수를 다루어 보아야 합니다.
이 함수는 단순히 블록의 값들을 활용하여 병합을 하는 단순한 역할을 수행하고 있습니다.
핵심 역할을 담당하는 부분이 될 함수 입니다.
func (pow *ProofOfWork) Run() (int, []byte) {
var hashInt big.Int
var hash [32]byte
max_number := ^uint(0)
nonce := 0
fmt.Printf("블록 마이닝 시작! %s\n", pow.block.Data)
for uint(nonce) < max_number {
data := pow.prepareData(nonce)
hash = sha256.Sum256(data)
fmt.Printf("\r%x", hash)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
fmt.Print("마이닝 성공")
return nonce, hash[:]
}
이 부분에 대해서 전반적으로 모두 이해를 하지는 못하였지만 천천히 다루어 보겠습니다.
일단 기본적인 변수들을 선언해 줍니다.
^uint(0)
을 활용하는 이유는 비트반전을 통해서 양의값중 가장 큰 값을 도출해 내기 위함입니다.이후 0부터 데이터를 확인해가면서 for문을 작동 시킵니다.
이떄 data는 prepareData
라는 함수에 의해서 []byte값을 가지게 됩니다.
그후 Sum256을 통해서 체크섬을 만들어 냅니다.
그후 수정된 해시값을 큰 정수로 변환을 하게 된뒤에 비교를 하게 됩니다.
이제 함수에서 nonce값을 활용을 하기 떄문에 구조체와 함수를 수정해 줍니다.
type Block struct {
Timestamp int64
Data []byte
PrevBlockHash []byte
Hash []byte
Nonce int
}
그후 함수는 이와 같이 수정 합니다.
func NewBlock(data string, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0}
pow := NewProofOwWork(block)
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}