티스토리 뷰
본 게시글에서는 저서 '밑바닥부터 시작하는 비트코인'의 Python으로 작성된 예제 코드를 Go로 컨버팅 하여 작성하였습니다.
📺 시리즈
2023.08.25 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 1장 유한체
2023.08.27 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 2장 타원곡선
2023.08.30 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 3장 타원곡선 암호
2023.09.02 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 4장 직렬화
2023.09.05 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 5장 트랜잭션
2023.09.11 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 6장 스크립트
2023.09.16 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 7장 트랜잭션 검증과 생성
2023.09.17 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 중간정리
2023.09.20 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 8장 p2sh 스크립트
🐱👤 전체 코드
🥅 학습 목표
1. 코인베이스 트랜잭션이 무엇이고 어떻게 구성되는지 알아봅니다.
2. 블록과 블록을 구성하는 블록헤더에 대해 알아봅니다.
3. 블록을 생성하기 위해 거쳐야 하는 작업증명에 대해 알아봅니다.
💰 1. 코인베이스 트랜잭션
- 정확히 하나의 입력을 가집니다.
- 입력의 이전 트랜잭션 해시는 32바이트의 0으로 설정합니다.
- 입력의 이전 트랜잭션 인덱스는 0xffffffff로 설정합니다.
다음은 트랜잭션이 코인베이스 트랜잭션인지 확인하는 Tx 구조체의 메서드입니다.
// 트랜잭션이 코인베이스 트랜잭션인지 여부를 반환하는 함수
func (t Tx) IsCoinbase() bool {
return len(t.Inputs) == 1 && // 입력 개수가 1이고
strings.EqualFold(t.Inputs[0].PrevTx, hex.EncodeToString(bytes.Repeat([]byte{0x00}, 32))) && // 이전 트랜잭션이 0x00으로 채워진 32바이트이고
t.Inputs[0].PrevIndex == 0xffffffff // 이전 트랜잭션의 출력 인덱스가 0xffffffff인 경우 코인베이스 트랜잭션
}
1.1 해제 스크립트
1.2 BIP0034 제안서
// 코인베이스 트랜잭션의 해제 스크립트에 포함된 높이를 반환하는 함수
func (t Tx) CoinbaseHeight() (bool, int) {
if !t.IsCoinbase() {
return false, 0
}
// 코인베이스 트랜잭션의 해제 스크립트에서 높이를 가져옴
scriptSig := t.Inputs[0].ScriptSig
heightBytes, ok := scriptSig.Cmds[0].([]byte)
if !ok {
return false, 0
}
return true, utils.LittleEndianToInt(heightBytes) // 리틀엔디언으로 인코딩된 높이를 반환
}
🚄 2. 블록 헤더
- 블록 버전 (4바이트, 리틀엔디언)
- 이전 블록의 해시 (32바이트, 리틀엔디언)
- 머클 루트 (32바이트, 리틀엔디언)
- 타임스탬프 (4바이트 리틀엔디언)
- 비트값 (4바이트 빅엔디언)
- 논스값 (4바이트 빅엔디언)
type Block struct {
Version int
PrevBlock string
MerkleRoot string
Timestamp int
Bits int
Nonce int
}
func New(version int, prevBlock, merkleRoot string, timestamp, bits, nonce int) *Block {
return &Block{
Version: version,
PrevBlock: prevBlock,
MerkleRoot: merkleRoot,
Timestamp: timestamp,
Bits: bits,
Nonce: nonce,
}
}
// 블록을 파싱하는 함수
func Parse(b []byte) (*Block, error) {
if len(b) < 80 {
return nil, errors.New("Block is too short")
}
buf := bytes.NewBuffer(b)
version := utils.LittleEndianToInt(buf.Next(4)) // 4바이트 리틀엔디언 정수
prevBlock := hex.EncodeToString(utils.ReverseBytes(buf.Next(32))) // 32바이트 리틀엔디언 해시
merkleRoot := hex.EncodeToString(utils.ReverseBytes(buf.Next(32))) // 32바이트 리틀엔디언 해시
timestamp := utils.LittleEndianToInt(buf.Next(4)) // 4바이트 리틀엔디언 정수
bits := utils.BytesToInt(buf.Next(4)) // 4바이트 리틀엔디언 정수
nonce := utils.BytesToInt(buf.Next(4)) // 4바이트 리틀엔디언 정수
return New(version, prevBlock, merkleRoot, timestamp, bits, nonce), nil
}
// 블록을 직렬화하는 함수
func (b *Block) Serialize() ([]byte, error) {
result := make([]byte, 0, 80)
version := utils.IntToLittleEndian(b.Version, 4) // version 4바이트 리틀엔디언
prevBlockBytes, err := hex.DecodeString(b.PrevBlock) // 16진수 문자열을 []byte로 변환
if err != nil {
return nil, err
}
prevBlock := utils.ReverseBytes(prevBlockBytes) // prevBlock 32바이트 리틀엔디언
merkleRootBytes, err := hex.DecodeString(b.MerkleRoot) // 16진수 문자열을 []byte로 변환
if err != nil {
return nil, err
}
merkleRoot := utils.ReverseBytes(merkleRootBytes) // merkleRoot 32바이트 리틀엔디언
timestamp := utils.IntToLittleEndian(b.Timestamp, 4) // timestamp 4바이트 리틀엔디언
bits := utils.IntToBytes(b.Bits, 4) // bits 4바이트 빅엔디언
nonce := utils.IntToBytes(b.Nonce, 4) // nonce 4바이트 빅엔디언
totalLength := len(version) + len(prevBlock) + len(merkleRoot) + len(timestamp) + len(bits) + len(nonce)
if totalLength > 80 {
return nil, errors.New("The size of block is too big")
}
result = append(result, version...)
result = append(result, prevBlock...)
result = append(result, merkleRoot...)
result = append(result, timestamp...)
result = append(result, bits...)
result = append(result, nonce...)
return result, nil
}
// 블록의 해시를 계산하는 함수
func (b *Block) Hash() ([]byte, error) {
s, err := b.Serialize()
if err != nil {
return nil, err
}
return utils.ReverseBytes(utils.Hash256(s)), nil
}
2.1 블록 버전
- 블록 버전 2는 소프트웨어가 BIP0034에 대한 지원(코인베이스 트랜잭션에 블록 높이를 넣음)을 추가했음을 의미
- 블록 버전 3은 BIP0066에 대한 지원(엄격한 DER 인코딩 시행)을 추가했음을 의미
- 블록 버전 4는 BIP0065에 대한 지원(OP_CHECKLOCKTIMEVERIFY 사용을 규정)을 추가했음을 의미
- 채굴자는 자신의 채굴 소프트웨어가 BIP0009 규정을 따른다는 것을 나타내기 위해 블록 버전 필드 4바이트 중 처음 3비트를 001로 설정합니다.
- 나머지 29비트는 어떤 기능이 준비되어 있는지를 나타내기 위해 1비트씩 사용하여 29개의 기능에 대한 준비 상황을 표시합니다.
func readBlockVersionBIP9() {
rawBlockHeader, _ := hex.DecodeString("020000208ec39428b17323fa0ddec8e887b4a7c53b8c0a0a220cfd0000000000000000005b0750fce0a889502d40508d39576821155e9c9e3f5c3157f961db38fd8b25be1e77a759e93c0118a4ffd71d")
b, _ := block.Parse(rawBlockHeader)
fmt.Println("BIP9:", b.Version>>29 == 0x001) // 처음 3비트가 001이면 BIP9 활성화
fmt.Println("BIP91:", b.Version>>4&1 == 1) // 4번째 비트가 1이면 BIP91 활성화
fmt.Println("BIP141:", b.Version>>1&1 == 1) // 2번째 비트가 1이면 BIP141 활성화
}
$ go run main.go
BIP9: true
BIP91: false
BIP141: true
2.2 이전 블록 해시값
2.3 머클 루트
2.4 타임스탬프
2.5 비트값
2.6 논스값
💡 3. 작업증명
블록 헤더의 해시값이 특정 조건을 만족하는 숫자보다 작아야 한다.
3.1 채굴자의 해시값 생성 방법
3.2 목푯값
target = coefficient × 256exponent-3
다음은 비트값을 목푯값으로 변환하는 함수입니다.
func BitsToTarget(b []byte) *big.Int {
exp := utils.BytesToBigInt(b[len(b)-1:]) // 지수
coef := utils.LittleEndianToBigInt(b[:len(b)-1]) // 계수, 리틀엔디언
return big.NewInt(0).Mul(coef, big.NewInt(0).Exp(big.NewInt(256), big.NewInt(0).Sub(exp, big.NewInt(3)), nil)) // 계수 * 256^(지수-3) = 목푯값
}
다음과 같은 목푯값이 있다면 이를 구하는 것은 얼마나 어려울까요?
0000000000000000013ce9000000000000000000000000000000000000000000
목푯값은 sha256 함수가 연속으로 두 번 적용된 것으로, 무작위의 값이라고 볼 수 있습니다. 이렇게 무작위로 출력된 256비트의 숫자에서 첫 번째 비트가 0일 확률은 1/2입니다. 처음 두 개의 비트가 00일 확률은 1/4이며, 0의 개수가 하나씩 늘어날수록 확률은 절반으로 줄어듭니다. 위의 목푯값은 처음 71비트가 0인 숫자입니다. 처음 71비트가 0일 확률은 1/271로 극히 작은 확률입니다. 문제는 값이 무작위이기 때문에 마땅한 공식이 없다는 것입니다. 즉, 무작위로 271번 해시값을 계산해야 찾을까 말까한 값이라는 것입니다.
3.3 난이도
difficulty = 0xffff * 2560x1d - 3 / target
다음은 난이도를 계산하는 Block 구조체의 메서드입니다.
// 블록의 난이도를 계산하는 함수
func (b Block) Difficulty() *big.Float {
target := BitsToTarget(utils.IntToBytes(b.Bits, 4))
return big.NewFloat(0).Mul(big.NewFloat(0xffff), big.NewFloat(0).Quo(big.NewFloat(0).SetInt(new(big.Int).Exp(big.NewInt(256), big.NewInt(0).Sub(big.NewInt(0x1d), big.NewInt(3)), nil)), big.NewFloat(0).SetInt(target))) // 0xffff * 256^(0x1d - 3) / target
}
3.4 작업증명 유효성 확인
// 작업증명의 유호성을 검증하는 함수
func (b Block) CheckProofOfWork() (bool, error) {
hash, err := b.Hash() // 블록의 해시를 계산
if err != nil {
return false, err
}
target := BitsToTarget(utils.IntToBytes(b.Bits, 4)) // 목푯값 계산
proof := new(big.Int).SetBytes(utils.ReverseBytes(hash)) // 블록의 해시를 little endian으로 변환한 뒤 big.Int로 변환
return proof.Cmp(target) == -1, nil // 블록의 해시가 목푯값보다 작으면 true, 크거나 같으면 false 반환
}
3.5 난이도 조정
time_differential = (난이도 조정 기간의 마지막 블록 타임스탬프) - (난이도 조정 기간의 첫 번째 블록 타임스탬프)
if time_differential > 8주:
new_target = previous_target * 8주 / 2주
else if time_differential < 3.5일:
new_target = previous_target * 3.5일 / 2주
else:
new_target = previous_target * time_differential / 2주
// 목푯값을 비트로 변환하는 함수
func TargetToBits(target *big.Int) []byte {
rawBytes := target.Bytes() // 목푯값을 []byte로 변환, 앞에 0은 제외됨
var exp int
var coef []byte
// 만약 rawBytes가 1로 시작하면 음수가 되므로 변환해줌
if rawBytes[0] > 0x7f {
exp = len(rawBytes) + 1 // 0x00을 추가했으므로 지수는 1 증가
coef = append([]byte{0x00}, rawBytes[:2]...) // 0x00을 추가해줌
} else {
exp = len(rawBytes) // 지수
coef = rawBytes[:3] // 계수
}
return append(utils.ReverseBytes(coef), byte(exp)) // 계수를 리틀엔디언으로 변환하고 지수를 뒤에 붙임
}
- 목푯값의 앞에 있는 모든 0을 제거합니다. big.Int 타입의 Bytes 메서드는 주어진 값만 바이트 슬라이스로 반환하므로 0을 지워줄 필요는 없습니다.
- 가장 왼쪽에 있는 바이트 값이 0x7f보다 작아야 합니다. 만약 이보다 크다면 최상위 비트가 1 임을 의미하며, 이는 곧 rawBytes의 값이 음수임을 의미합니다. 목푯값은 항상 양수여야 하므로 계수의 앞에 1바이트 0x00을 붙여 양수로 만들어주고 지수 값도 보정합니다.
- 지수는 rawBytes의 0이 아닌 수의 자릿수를 의미하며 rawBytes의 길이와 같습니다.
- 계수는 rawBytes의 처음 세 바이트 값입니다.
- 비트값의 형식에 따라 계수를 리틀엔디언으로 변환하고 그 뒤에 지수를 붙여 총 4바이트 크기의 비트값을 반환합니다.
목푯값을 비트값으로 변환할 수 있으니 이를 종합하여 새로운 비트값을 계산하는 함수를 다음과 같이 작성할 수 있습니다.
func CalculateNewBits(prevBits []byte, timeDiff int64) []byte {
if timeDiff > TWO_WEEK*4 {
timeDiff = TWO_WEEK * 4
} else if timeDiff < TWO_WEEK/4 {
timeDiff = TWO_WEEK / 4
}
newTarget := big.NewInt(0).Div(big.NewInt(0).Mul(BitsToTarget(prevBits), big.NewInt(timeDiff)), big.NewInt(TWO_WEEK))
return TargetToBits(newTarget)
}
📖 참고자료
글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
'블록체인 > Bitcoin' 카테고리의 다른 글
밑바닥부터 시작하는 비트코인 - 11장 단순 지급 검증 (1) | 2024.01.09 |
---|---|
밑바닥부터 시작하는 비트코인 - 10장 네트워킹 (1) | 2024.01.08 |
밑바닥부터 시작하는 비트코인 - 8장 p2sh 스크립트 (0) | 2023.09.20 |
밑바닥부터 시작하는 비트코인 - 7장 트랜잭션 검증과 생성 (1) | 2023.09.16 |
밑바닥부터 시작하는 비트코인 - 6장 스크립트 (0) | 2023.09.11 |