티스토리 뷰

 

본 게시글에서는 저서 '밑바닥부터 시작하는 비트코인'의 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장 스크립트


🐱‍👤 전체 코드

 

GitHub - piatoss3612/bitcoin-from-scratch: 밑바닥부터 시작하는 비트코인을 읽고 Go로 구현해보는 프로젝

밑바닥부터 시작하는 비트코인을 읽고 Go로 구현해보는 프로젝트. Contribute to piatoss3612/bitcoin-from-scratch development by creating an account on GitHub.

github.com


🔍 1. 트랜잭션 검증

 네트워크로 전파된 트랜잭션이 블록에 추가되기 위해서는 다음과 같은 항목들에 대한 검증이 필요합니다.

 

  1. 트랜잭션의 각 입력이 참조하는 출력값이 존재해야 하며 소비되지 않은 상태여야 합니다.
  2. 입력 비트코인의 합이 출력 비트코인의 합보다 크거나 같은지 확인합니다.
  3. 각 입력이 참조하는 출력의 잠금 스크립트를 입력의 해제 스크립트로 해제할 수 있어야 합니다.

1. 1 입력 비트코인 존재 확인

 1번은 이중 지불을 방지하기 위한 항목입니다. 트랜잭션 자체만으로는 이중 지불 여부를 확인할 수 없습니다. 이중 지불을 확인하기 위해서는 전체 트랜잭션 집합으로부터 계산된 UTXO 집합을 뒤져봐야 합니다. 만약 입력이 가리키는 UTXO가 집합에 존재하면 이중 지불되지 않은 것이며, 검증을 마친 트랜잭션의 입력이 가리키는 모든 UTXO는 UTXO 집합으로부터 제거됩니다.

1.2 입력과 출력 비트코인 합계 확인

 2번은 새로운 비트코인이 만들어지는 것을 방지하기 위한 항목입니다. 이를 위해 입력 비트코인의 합이 출력 비트코인의 합보다 크거나 같은지 확인합니다. 만약 수수료(입력 비트코인의 합 - 출력 비트코인의 합)가 음숫값이라면 존재하지 않는 출력 비트코인이 포함된 것이므로 해당 트랜잭션이 블록체인에 추가된다면 존재하지 않던 비트코인이 네트워크 상에 생성됩니다. 이를 허용하면 안 되므로 반드시 수수료를 확인하고 그 값이 음수라면 트랜잭션을 거부해야 합니다. 한 가지 예외 상황으로 코인베이스 트랜잭션이 존재하는데, 이는 9장에서 다룹니다.

1.3 서명 확인

 3번은 해제 스크립트에 들어간 서명이 유효한지 확인하는 항목입니다. ECDSA 서명 알고리즘은 공개키 P,  서명해시 z 그리고 서명 (r, s)가 필요합니다. 사용하려는 출력의 잠금 스크립트에 들어있는 공개키에 대응되는 비밀키를 가진 사용자라면 서명은 쉽게 알아낼 수 있습니다. 문제는 서명을 알아내기 위해 서명해시 z를 어떻게 계산해야 하냐는 것입니다. 직렬화된 트랜잭션에 hash256 함수를 적용한 값을 서명해시로 사용할 수 있을 것 같지만, 이는 정확한 서명해시가 아닙니다. 이렇게 하면 서명을 구하기 위해 필요한 서명해시를 만들기 위해 서명 자체가 들어가야 한다는 모순이 발생합니다.

 

 따라서 트랜잭션을 아래와 같은 단계로 변형해야 합니다. 입력이 여러 개라면 각 입력에 대한 서명해시를 구해야 합니다.

1단계: 모든 해제 스크립트를 비운다.

 서명을 검증할 때 먼저 트랜잭션 안에 모든 해제 스크립트를 삭제합니다. 서명을 생성할 때도 마찬가지입니다.

2단계: 삭제된 해제 스크립트 자리에 사용할 UTXO의 잠금 스크립트를 넣는다.

 이전 트랜잭션 출력의 잠금 스크립트를 제거한 해제 스크립트 자리에 삽입합니다. 잠금 스크립트를 찾기 위해 UTXO 집합을 참고해도 되지만, 서명 생성자의 공개키를 사용하여 손쉽게 생성할 수도 있습니다. 

3단계: 해시 유형을 덧붙인다.

 해시 유형은 4바이트 리틀엔디언 정수로, 트랜잭션의 마지막에 덧붙입니다. 이 정보로 서명이 어떤 용도로 사용되는지 알 수 있습니다.

해시 유형 용도
SIGHASH_ALL 현재의 입력과 다른 모든 입출력을 함께 인증하며 가장 일반적인 해시 유형. 1을 4바이트 리틀엔디언으로 표현
SIGHASH_SINGLE 현재의 입력과 이에 대응되는 출력과 다른 모든 입력도 함께 인증. 3을 4바이트 리틀엔디언으로 표현
SIGHASH_NONE 현재의 입력과 다른 모든 입력을 인증. 2를 4바이트 리틀엔디언으로 표현
SIGHASH_ANYONECANPAY 일단 이런 것이 있다는 정도만 알아둡시다

 여기서 '인증한다'의 의미는 서명해시를 구하기 위한 메시지에 인증되는 필드를 포함시킨다는 뜻입니다.

 

 이렇게 변경된 트랜잭션을 hash256 함수에 넣어 얻은 해시값을 구하고 이를 다시 32바이트 빅엔디언 정수로 변환하면 최종적으로 서명해시 z를 구할 수 있습니다. 서명해시를 구하는 과정을 코드로 작성하면 다음과 같습니다.

// 트랜잭션의 서명해시를 반환하는 함수
func (t Tx) SigHash(inputIndex int) ([]byte, error) {
	// 입력 인덱스가 트랜잭션의 입력 개수보다 크면 에러를 반환
	if inputIndex >= len(t.Inputs) {
		return nil, fmt.Errorf("input index %d greater than the number of inputs %d", inputIndex, len(t.Inputs))
	}

	s := utils.IntToLittleEndian(t.Version, 4) // 버전

	in, err := t.serializeInputsForSig(inputIndex) // 입력 목록
	if err != nil {
		return nil, err
	}

	s = append(s, in...)

	out, err := t.serializeOutputs() // 출력 목록
	if err != nil {
		return nil, err
	}

	s = append(s, out...)

	s = append(s, utils.IntToLittleEndian(t.Locktime, 4)...) // 유효 시점

	s = append(s, utils.IntToLittleEndian(SIGHASH_ALL, 4)...) // SIGHASH_ALL (4바이트)

	h256 := utils.Hash256(s) // 해시를 생성

	return h256, nil // 해시를 반환
}

// 서명해시를 만들 때 사용할 입력 목록을 직렬화한 결과를 반환하는 함수
func (t Tx) serializeInputsForSig(inputIndex int) ([]byte, error) {
	inputs := t.Inputs

	result := utils.EncodeVarint(len(inputs)) // 입력 개수

	for i, input := range inputs {
		if i == inputIndex { // 입력 인덱스가 inputIndex와 같으면
			scriptPubKey, err := input.ScriptPubKey(NewTxFetcher(), t.Testnet) // 이전 트랜잭션 출력의 잠금 스크립트를 가져옴
			if err != nil {
				return nil, err
			}

			newInput := NewTxIn(input.PrevTx, input.PrevIndex, scriptPubKey, input.SeqNo) // 이전 트랜잭션 출력의 잠금 스크립트를 사용하는 새로운 입력을 생성
			s, err := newInput.Serialize()                                                // 새로운 입력을 직렬화
			if err != nil {
				return nil, err
			}

			result = append(result, s...) // 직렬화한 결과를 result에 추가
		} else { // 입력 인덱스가 inputIndex와 다르면
			s, err := NewTxIn(input.PrevTx, input.PrevIndex, nil, input.SeqNo).Serialize() // 해제 스크립트가 비어있는 새로운 입력을 생성하고 직렬화
			if err != nil {
				return nil, err
			}

			result = append(result, s...) // 직렬화한 결과를 result에 추가
		}
	}

	return result, nil // 직렬화한 결과를 반환
}

 트랜잭션 입력을 검증하는 함수는 다음과 같습니다.

// 트랜잭션의 입력을 검증하는 함수
func (t Tx) VerifyInput(inputIndex int) (bool, error) {
	if inputIndex >= len(t.Inputs) {
		return false, fmt.Errorf("input index %d greater than the number of inputs %d", inputIndex, len(t.Inputs))
	}

	input := t.Inputs[inputIndex] // 입력을 가져옴

	scriptSig := input.ScriptSig // 해제 스크립트

	scriptPubKey, err := input.ScriptPubKey(NewTxFetcher(), t.Testnet) // 이전 트랜잭션 출력의 잠금 스크립트를 가져옴
	if err != nil {
		return false, err
	}

	z, err := t.SigHash(inputIndex) // 서명해시를 가져옴
	if err != nil {
		return false, err
	}

	combined := scriptSig.Add(scriptPubKey) // 해제 스크립트와 잠금 스크립트를 결합

	return combined.Evaluate(z) // 결합한 스크립트를 평가
}

1.4 전체 트랜잭션 검증

입력을 검증할 수 있으니 전체 트랜잭션은 다음과 같이 실행할 수 있습니다.

// 트랜잭션을 검증하는 함수
func (t Tx) Verify() (bool, error) {
	fee, err := t.Fee(NewTxFetcher()) // 수수료를 가져옴
	if err != nil {
		return false, err
	}

	// 수수료가 음수이면 유효하지 않은 트랜잭션
	if fee < 0 {
		return false, nil
	}

	// 트랜잭션의 입력을 검증
	for i := 0; i < len(t.Inputs); i++ {
		ok, err := t.VerifyInput(i)
		if err != nil {
			return false, err
		}

		if !ok {
			return false, nil
		}
	}

	return true, nil
}

 풀 노드는 실제로 더 많은 검증을 수행합니다. 예를 들어 이중 지불 확인, 합의 규칙 확인, 트랜잭션의 유효 시점 확인 등이 있습니다.


🔨 2. 트랜잭션 생성

 트랜잭션 검증에 사용한 코드를 활용하여 검증 항목에 부합하는 유효한 트랜잭션을 생성할 수 있다. 요컨대 생성된 트랜잭션의 입력의 합은 출력의 합보다 크거나 같아야 하고, 잠금 스트립트를 해제 스크립트로 해제할 수 있어야 한다.

 

 트랜잭션을 생성하려면 최소한 하나의 UTXO를 참조해야 한다. 또한 UTXO의 잠금 스크립트를 해제할 수 있는 서명을 생성하기 위한 비밀키도 필요합니다.

2.1 트랜잭션 설계

트랜잭션을 설계하려면 다음 기본 질문에 답할 수 있어야 합니다.

 

  1. 비트코인을 어느 주소로 보낼 것인가?
  2. 어느 UTXO를 사용할 것인가? (비트코인이 얼마나 있는가?)
  3. 수수료는 얼마로 할 것인가? (얼마나 빨리 처리되길 원하는가?)

2.2 트랜잭션 구성

 트랜잭션을 구성하기 위해서는 먼저 Base58로 표현된 주소로부터 20바이트 해시를 얻어야 합니다. 이 과정은 다음 함수를 사용하면 됩니다.

var base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" // base58 인코딩에 사용할 문자열

// base58로 인코딩된 문자열을 바이트 슬라이스로 디코딩하는 함수
func DecodeBase58(s string) ([]byte, error) {
	result := big.NewInt(0) // 결과를 저장할 big.Int

	for _, c := range s {
		// base58Alphabet에서 c의 인덱스를 찾음
		charIndex := strings.IndexByte(base58Alphabet, byte(c))

		// 58을 곱하고 인덱스를 더함
		result.Mul(result, big.NewInt(58))
		result.Add(result, big.NewInt(int64(charIndex)))
	}

	combined := result.FillBytes(make([]byte, 25)) // 크기가 25인 바이트 슬라이스를 만들어 big.Int를 채움
	checksum := combined[len(combined)-4:]         // 마지막 4바이트는 체크섬

	// 체크섬 검사
	if !bytes.Equal(Hash256(combined[:len(combined)-4])[:4], checksum) {
		return nil, fmt.Errorf("bad address: %s %s", checksum, Hash256(combined[:len(combined)-4])[:4])
	}

	return combined[1 : len(combined)-4], nil // prefix를 제외하고 체크섬을 제외한 바이트 슬라이스를 반환
}

 또한 20바이트 해시값을 잠금 스크립트로 변환하기 위해 다음 함수를 사용합니다. 이 함수는 20바이트 해시값을 입력으로 받아서 p2pkh 스크립트를 반환합니다.

func NewP2PKHScript(h160 []byte) *Script {
	return New(
		0x76, // OP_DUP
		0xa9, // OP_HASH160
		h160, // 20바이트의 데이터
		0x88, // OP_EQUALVERIFY
		0xac, // OP_CHECKSIG
	)
}

 이렇게 주어진 함수들을 사용하여 아래와 같이 트랜잭션을 구성할 수 있습니다.

package main

import (
	"chapter07/script"
	"chapter07/tx"
	"chapter07/utils"
	"fmt"
)

func main() {
	checkGenTx()
}

func checkGenTx() {
	prevTx := "0d6fe5213c0b3291f208cba8bfb59b7476dffacc4e5cb66f6eb20a080843a299" // 이전 트랜잭션 ID
	prevIndex := 13                                                              // 이전 트랜잭션의 출력 인덱스
	txIn := tx.NewTxIn(prevTx, prevIndex, nil)                                   // 트랜잭션 입력 생성 (해제 스크립트는 비어있음)

	changeAmount := int(0.33 * 1e8)                                           // 출력 금액
	changeH160, _ := utils.DecodeBase58("mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2") // 잠금 스크립트를 생성할 주소
	changeScript := script.NewP2PKHScript(changeH160)                         // p2pkh 잠금 스크립트 생성
	changeOutput := tx.NewTxOut(changeAmount, changeScript)                   // 트랜잭션 출력 생성

	targetAmount := int(0.1 * 1e8)                                            // 출력 금액
	targetH160, _ := utils.DecodeBase58("mnrVtF8DWjMu839VW3rBfgYaAfKk8983Xf") // 잠금 스크립트를 생성할 주소
	targetScript := script.NewP2PKHScript(targetH160)                         // p2pkh 잠금 스크립트 생성
	targetOutput := tx.NewTxOut(targetAmount, targetScript)                   // 트랜잭션 출력 생성

	txObj := tx.NewTx(1, []*tx.TxIn{txIn}, []*tx.TxOut{changeOutput, targetOutput}, 0, true) // 트랜잭션 생성
	fmt.Println(txObj)
}
$ go run main.go
tx: cd30a8da777d28ef0e61efe68a9f7c559c1d3e5bcd7b265c850ccb4068598d11
version: 1
inputs: [0d6fe5213c0b3291f208cba8bfb59b7476dffacc4e5cb66f6eb20a080843a299:13]
outputs: [33000000:OP_DUP OP_HASH160 d52ad7ca9b3d096a38e752c2018e6fbc40cdf26f OP_EQUALVERIFY OP_CHECKSIG  10000000:OP_DUP OP_HASH160 507b27411ccf7f16f10297de6cef3f291623eddf OP_EQUALVERIFY OP_CHECKSIG ]
locktime: 0

 이렇게 생성된 트랜잭션은 아직 유효하지 않습니다. 트랜잭션 입력에 대응되는 이전 트랜잭션 출력의 잠금 스크립트를 해제할 해제 스크립트가 비어있기 때문입니다. 따라서 해제 스크립트를 생성해야 합니다.

2.3 트랜잭션 해제 스크립트 생성

 잠금 스크립트 안에 해싱된 공개키가 포함되어 있으므로, 이에 대응하는 비밀키를 사용하여 서명해시 z와 이에 대한 DER 형식의 서명을 생성할 수 있습니다.

package main

import (
	"chapter07/ecc"
	"chapter07/script"
	"chapter07/tx"
	"encoding/hex"
	"fmt"
	"math/big"
)

func main() {
	checkGenScriptSig()
}

func checkGenScriptSig() {
	rawTx, _ := hex.DecodeString("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600")
	parsedTx, _ := tx.ParseTx(rawTx, false) // 트랜잭션 파싱

	z, _ := parsedTx.SigHash(0)                                         // 서명 해시 생성
	privateKey, _ := ecc.NewS256PrivateKey(big.NewInt(8675309).Bytes()) // 개인 키 생성
	point, _ := privateKey.Sign(z)                                      // 서명 생성
	der := point.DER()                                                  // 서명을 DER 형식으로 변환
	sig := append(der, byte(tx.SIGHASH_ALL))                            // DER 서명에 해시 타입을 추가
	sec := privateKey.Point().SEC(true)                                 // 공개 키를 압축된 SEC 형식으로 변환
	scriptSig := script.New(sig, sec)                                   // 해제 스크립트 생성
	parsedTx.Inputs[0].ScriptSig = scriptSig                            // 해제 스크립트를 트랜잭션 입력에 추가
	encoded, err := parsedTx.Serialize()                                // 트랜잭션 직렬화
	if err != nil {
		panic(err)
	}

	fmt.Println(hex.EncodeToString(encoded)) // 직렬화된 트랜잭션 출력
}
$ go run main.go
0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006a47304402207db2402a3311a3b845b038885e3dd889c08126a8570f26a844e3e4049c482a11022010178cdca4129eacbeab7c44648bf5ac1f9cac217cd609d216ec2ebc8d242c0a012103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b67feffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600

📡 3. 테스트넷 트랜잭션 생성 및 전파

3.1 테스트넷 주소 생성

 테스트넷 주소를 생성하기 위해 필요한 비밀키는 본인이 원하는 값으로 자유롭게 선택하면 됩니다.

secret := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss rules the world")))
privateKey, _ := ecc.NewS256PrivateKey(secret.Bytes())

address := privateKey.Point().Address(true, true)

3.2 테스트넷 비트코인 받기

이제 구글에 testnet bitcoin faucet을 검색해 보면 테스트 비트코인을 받을 수 있는 사이트를 찾을 수 있습니다. 그중 한 곳에 들어가서 생성된 테스트넷 주소를 입력하면 테스트넷 비트코인을 받을 수 있습니다. 제가 사용한 주소는 https://coinfaucet.eu/en/btc-testnet/ 입니다.

테스트넷 비트코인 받기

 테스트넷 비트코인을 받고 나면 https://live.blockcypher.com/btc-testnet/address/<테스트넷 주소>로 들어가서 잔액을 확인할 수 있습니다.

테스트넷 비트코인 잔액 확인

3.3 트랜잭션 생성

이제 트랜잭션을 생성해 보겠습니다. 테스트넷 비트코인을 받을 때 생성된 트랜잭션을 참조해서 이전 트랜잭션의 ID와 사용할 출력의 인덱스를 가져옵니다.

테스트넷 비트코인을 받을 때 생성된 트랜잭션

 그리고 다음 코드를 사용해서 직렬화된 트랜잭션을 생성합니다.

func checkGenTestnetTx() {
	secret := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss rules the world"))) // 개인 키 생성
	privateKey, _ := ecc.NewS256PrivateKey(secret.Bytes())

	address := privateKey.Point().Address(true, true)

	prevTx := "e770e0b481166da7d0d139c855e86633a12dbd4fa9b97f33a31fc9a458f8ddd7" // 이전 트랜잭션 ID
	prevIndex := 0                                                               // 이전 트랜잭션의 출력 인덱스
	txIn := tx.NewTxIn(prevTx, prevIndex, nil)

	balance := 1193538 // 잔고

	changeAmount := balance - (balance * 6 / 10)            // 잔액
	changeH160, _ := utils.DecodeBase58(address)            // 잔액을 받을 주소
	changeScript := script.NewP2PKHScript(changeH160)       // p2pkh 잠금 스크립트 생성
	changeOutput := tx.NewTxOut(changeAmount, changeScript) // 트랜잭션 출력 생성

	targetAmount := balance * 6 / 10                                          // 사용할 금액
	targetH160, _ := utils.DecodeBase58("mwJn1YPMq7y5F8J3LkC5Hxg9PHyZ5K4cFv") // 받을 주소
	targetScript := script.NewP2PKHScript(targetH160)                         // p2pkh 잠금 스크립트 생성
	targetOutput := tx.NewTxOut(targetAmount, targetScript)                   // 트랜잭션 출력 생성

	txObj := tx.NewTx(1, []*tx.TxIn{txIn}, []*tx.TxOut{changeOutput, targetOutput}, 0, true) // 트랜잭션 생성

	ok, err := txObj.SignInput(0, privateKey, true) // 서명 생성
	if err != nil {
		panic(err)
	}

	fmt.Println(ok)

	serializedTx, err := txObj.Serialize() // 트랜잭션 직렬화
	if err != nil {
		panic(err)
	}

	fmt.Println(hex.EncodeToString(serializedTx))
}
$ go run main.go
true
0100000001d7ddf858a4c91fa3337fb9a94fbd2da13366e855c839d1d0a76d1681b4e070e7000000006b4830450221009f1546297fb02e2385cb352bd999f52f31ff8ca2851f0df5b0fe168891c92516022043e7bccacbaca8863962132a957994c09b14e5eaceb47f5b95b074b95cc54ff3012103a7005a25ae9cf0ed9804d4d5f1b0bea6d7b8e901dd4bfa4e21d0914b7e195d74ffffffff02e8480700000000001976a914bb55f73b3c61e3c4e45bf2466a67109652cde9bf88ac5aed0a00000000001976a914ad346f8eb57dee9a37981716e498120ae80e44f788ac00000000

3.4 트랜잭션 전파

직렬화한 트랜잭션을 https://live.blockcypher.com/btc-testnet/pushtx/ 에서 전파하면 테스트넷에 트랜잭션이 전파됩니다.

트랜잭션 전파

그러면 이렇게 트랜잭션이 전파된 것을 확인할 수 있습니다. 그런데 한 가지 실수한 것이 있습니다. 

트랜잭션 전파 확인

 그것은 바로 트랜잭션 수수료를 전혀 고려하지 않았다는 것입니다. 이 때문에 트랜잭션이 전파는 되었지만, 블록에는 추가가 되지 못하고 있는 상태입니다. 이 부분은 앞으로 주의하도록 하겠습니다...

수수료를 주의합시다...

 그래도 일단은 테스트넷 주소에 들어있는 잔액에는 변화가 생긴 것 같습니다. 

테스트넷 비트코인 잔액 확인2

 테스트넷에 트랜잭션을 전파하는 실습을 여기서 마무리하겠습니다. 다음장에서는 p2sh 스크립트에 대해 다룰 예정입니다.


💯 문제해결

1. 서명을 생성할 때 이상한 k값을 생성하는 문제

 트랜잭션을 생성하는 과정에서 완전히 이상한 서명값이 나오길래 디버깅을 해보니 k값이 예상한 것과 완전히 다른 값으로 계산되는 문제를 발견했습니다. 해당 문제는 서명해시와 비밀키의 값 자체를 바이트 슬라이스로 가져오는 것이 아닌 크기가 32인 바이트 슬라이스에 채워 넣는 식으로 변경하여 해결했습니다.

// Before
zBytes := z.Bytes()
secreteBytes := pvk.secret

// After
zBytes := z.FillBytes(make([]byte, 32))
secreteBytes := big.NewInt(0).SetBytes(pvk.secret).FillBytes(make([]byte, 32))

2. DER 형식의 서명을 역직렬화할 때 해시 유형으로 인해 길이 비교가 실패하는 문제

이 문제는 일단 DER 형식의 서명 뒤에 해시 유형이 왜 붙는지 이해가 아직 덜 됐기 때문에 임시방편으로 길이 비교하는 부분을 주석처리했습니다. 그러고 나니 다른 것들은 모두 정상적으로 동작하더군요. 해시 유형을 직렬화한 트랜잭션 뒤에 4바이트 리틀엔디언으로 붙이는 건 알겠는데 DER 형식의 서명 뒤에는 왜 넣는 걸까요? 그리고 파이썬 코드에서는 길이 비교하는 부분이 왜 정상적으로 돌아갈까요? 좀 더 공부를 하고 돌아오겠습니다...


📖 참고자료

 

밑바닥부터 시작하는 비트코인

비트코인은 블록체인 기술의 집약체입니다. 이더리움, 이오스 같은 2, 3세대 블록체인은 비트코인을 바탕으로 확장, 발전한 개념입니다. 디앱 개발에서 머무르지 않고 블록체인 개발자로 성장하

www.hanbit.co.kr

 

GitHub - jimmysong/programmingbitcoin: Repository for the book

Repository for the book. Contribute to jimmysong/programmingbitcoin development by creating an account on GitHub.

github.com

글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
최근에 올라온 글
최근에 달린 댓글
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Total
Today
Yesterday
글 보관함