티스토리 뷰
본 게시글에서는 저서 '밑바닥부터 시작하는 비트코인'의 Python으로 작성된 예제 코드를 Go로 컨버팅 하여 작성하였습니다.
📺시리즈
2023.08.25 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 1장 유한체
2023.08.27 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 2장 타원곡선
2023.08.30 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 3장 타원곡선 암호
2023.09.02 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 4장 직렬화
2023.09.05 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 5장 트랜잭션
2023.09.11 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 6장 스크립트
2023.09.16 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 7장 트랜잭션 검증과 생성
2023.09.20 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 8장 p2sh 스크립트
2023.09.21 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 9장 블록
2024.01.08 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 10장 네트워킹
2024.01.09 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 11장 단순 지급 검증
2024.01.10 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 12장 블룸 필터
2024.01.13 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 13장 세그윗 1
2024.01.14 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 13장 세그윗 2
🐲 전체 코드
🩻 1차 시도
이전에 피어 노드로부터 메시지를 읽어 들이는데 아무리 큰 버퍼를 사용하더라도 페이로드가 뚝뚝 끊어져서 들어오는 문제로 크기가 16만 바이트 정도되는 headers 메시지를 읽어 들일 수 없었던 문제가 있었습니다.
func (sn *SimpleNode) Read() (*NetworkEnvelope, error) {
// 너무 작은 버퍼를 사용해서 데이터를 전부 읽어오지 못하는 문제가 있음 (책에서 파이썬으로 구현한 코드는 그런 문제가 없음)
// 32MB 버퍼를 사용해도 안됨
buf := make([]byte, 32*1024*1024)
n, err := sn.conn.Read(buf)
if err != nil {
return nil, err
}
envelope, err := ParseNetworkEnvelope(buf[:n])
if err != nil {
return nil, err
}
return envelope, nil
}
도대체 뭐가 문제인가 싶어서 파이썬 예제를 다시 살펴보니 제가 간과한 부분이 있었습니다.
stream = socket.makefile('rb', None)
while True:
new_message = NetworkEnvelope.parse(stream)
...
socket.makefile 메서드를 사용해서 stream이라는 변수를 생성하는 부분입니다. 제가 파이썬은 잘 모르지만 이 부분이 소켓으로부터 데이터를 읽어서 임시로 저장하는 파일(stream)을 만들어 반환해 주면 그것으로부터 데이터를 읽을 수 있다는 것을 추론할 수 있었습니다. 이걸 go에서 어떻게 구현하느냐가 문제입니다만...
🩼 2차 시도
그래도 일단 뚝심 있게 가보자 싶어서 아래와 같이 네트워크 메시지의 시작 부분(네트워크 매직, 명령어, 페이로드 길이, 페이로드 체크썸)을 감지하면 전체 메시지의 길이만큼 데이터를 읽어 들여 한 번에 파싱 하여 반환하였습니다.
func (sn *SimpleNode) ReadAll() (*NetworkEnvelope, error) {
buf := make([]byte, 1024)
n, err := sn.conn.Read(buf)
if err != nil {
return nil, err
}
if n < 4+12+4+4 { // magic + command + payload length + checksum (네트워크 메시지의 시작부분보다 작은 길이를 읽어온 경우)
return nil, ErrInvalidNetworkMessage
}
magic := buf[:4] // 가장 처음 4바이트는 네트워크 매직
if !IsNetworkMagicValid(magic) { // magic이 유효하지 않은 경우
fmt.Printf("Invalid network magic: %x\n", magic)
return nil, ErrInvalidNetworkMagic
}
payloadLength := utils.LittleEndianToInt(buf[16:20]) // 페이로드 길이를 읽어옴
totalLength := 4 + 12 + 4 + payloadLength + 4 // magic + command + payload length + payload + checksum (메시지의 전체 길이)
readCnt := n
data := make([]byte, 0, totalLength) // data를 메시지의 전체 길이로 초기화
data = append(data, buf[:n]...)
fmt.Println("Total Length:", totalLength, "Read Count:", readCnt)
for readCnt < totalLength {
n, err := sn.conn.Read(buf) // n은 읽어온 데이터의 길이
if err != nil {
return nil, err
}
readCnt += n
data = append(data, buf[:n]...)
fmt.Println("Total Length:", totalLength, "Read Count:", readCnt)
}
fmt.Printf("Remaining Data: %x\n", data[totalLength:])
envelope, err := ParseNetworkEnvelope(data) // data를 파싱해서 네트워크 메시지를 생성
if err != nil {
return nil, err
}
return envelope, nil
}
그래도 이번에는 잘 되는가 싶더니 데이터를 읽어 들일 때 다른 메시지까지 같이 읽어 들이는 바람에 아래 콘솔에 찍힌 것과 같이 남아서 버려지는 데이터가 발생했습니다.
또한 데이터를 읽어 들이는 중간에 패닉이 발생했는데 이는 WaitFor 메서드를 여러 개를 실행하는 바람에 여러 개의 goroutine에서 읽기를 시도해서 발생한 문제였습니다. 이 문제는 다음과 같이 WaitFor 메서드에 done이라는 이름의 받기 전용 채널을 추가하여 select문에서 빈 스트럭트를 받으면 goroutine을 종료하도록 수정했습니다.
func (sn *SimpleNode) WaitFor(commands []Command, done <-chan struct{}) (<-chan *NetworkEnvelope, <-chan error)
이걸 수정하고 나니 이번에는 headers 메시지를 파싱 하는 과정에서 문제가 발생했습니다.
이 부분은 제가 버퍼로부터 헤더를 읽고 제거를 안 해줘 발생한 오류로 아래와 같이 수정했습니다.
이렇게 수정하고 나니 headers 메시지를 정상적으로 파싱 하는 것을 확인할 수 있었습니다.
🔍 10장 블록 헤더 응답 검증
비트코인 메인넷에서 실행 중인 노드로 getheaders 메시지를 보내고 headers 응답을 받아 블록을 검증하는 과정입니다. 연결 가능한 메인넷 주소는 아래의 사이트에서 확인이 가능합니다.
교재의 예제는 메인넷으로 연결하고 있지만, 어째서인지 메인넷으로 연결하려고 하면 핸드셰이크까지만 진행이 되고 getheaders 메시지를 보내자마자 상대방이 연결을 끊어버리니 뭐가 어떻게 돌아가는지 알 수가 없습니다. 그래서 일단은 테스트넷에 연결하여 진행을 했습니다. 다만 테스트넷과 메인넷의 블록 채굴 및 비트값 계산이 상이하기 때문에 아래 코드에서 비트값을 계산하는 부분은 유효하지 않다고 생각하시면 됩니다. 사실은 getheaders 요청으로 가져오는 헤더의 개수가 최대 2천 개 이므로 제네시스 블록부터 2천 번째 블록까지는 비트값이 동일합니다.
func VerifyBlockHeaders() {
rawGenesisBlock, _ := hex.DecodeString("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18") // testnet
// rawGenesisBlock, _ := hex.DecodeString("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c") // mainnet
previous, _ := block.Parse(rawGenesisBlock)
node, err := network.NewSimpleNode("71.13.92.62", 18333, network.TestNet, true) // testnet
// node, err := network.NewSimpleNode("165.227.133.233", 8333, network.MainNet, true) // mainnet
if err != nil {
log.Fatal(err)
}
defer node.Close()
log.Println("Connected to", node.Host, "on port", node.Port)
resp, err := node.HandShake()
if err != nil {
log.Fatal(err)
}
if ok := <-resp; !ok {
log.Fatal("Handshake failed")
}
time.Sleep(1 * time.Second)
getheaders := network.DefaultGetHeadersMessage()
hexPreviousHash, _ := previous.Hash()
previousHash := hex.EncodeToString(hexPreviousHash)
firstEpochTimestamp := previous.Timestamp
expectedBits, _ := hex.DecodeString("ffff001d")
getheaders.StartBlock = hexPreviousHash
err = node.Send(getheaders, network.TestNet)
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
envelopes, errs := node.WaitFor([]network.Command{network.HeadersCommand}, done)
go func() {
defer close(done)
for {
select {
case err := <-errs:
if err == io.EOF {
log.Println("Connection closed")
return
}
log.Fatalf("Error receiving message: %s", err)
case headers := <-envelopes:
msg, err := network.ParseHeadersMessage(headers.Payload)
if err != nil {
log.Fatalf("Error parsing headers message: %s", err)
}
fmt.Println("Received headers message with", len(msg.Headers), "headers")
count := 1
for i, header := range msg.Headers {
// 작업 증명 검증
ok, err := header.CheckProofOfWork()
if err != nil {
log.Fatalf("Error checking proof of work for block header %d: %s", i, err)
}
if !ok {
log.Fatalf("Block header %d does not satisfy proof of work", i)
}
// 이전 블록 해시 검증
if !strings.EqualFold(header.PrevBlock, previousHash) {
log.Fatalf("Block header %d's previous hash is not correct", i)
}
// 블록 난이도 계산
if count%2016 == 0 {
timeDiff := previous.Timestamp - firstEpochTimestamp
expectedBits = block.CalculateNewBits(utils.IntToBytes(previous.Bits, 4), int64(timeDiff))
firstEpochTimestamp = previous.Timestamp
fmt.Println("New epoch, expected bits are", hex.EncodeToString(expectedBits))
}
// 블록 난이도 검증
if header.Bits != utils.BytesToInt(expectedBits) {
log.Fatalf("Block header %d's bits are not correct: expected %d, got %d", i, utils.BytesToInt(expectedBits), header.Bits)
}
hexHash, err := header.Hash()
if err != nil {
log.Fatalf("Error hashing block header %d: %s", i, err)
}
hash := hex.EncodeToString(hexHash)
previousHash = hash
count++
}
return
}
}
}()
<-done
fmt.Println("Done")
}
코드를 실행하면 오류 없이 'Done'이 정상적으로 출력되는 것을 확인할 수 있습니다.
🩺 3차 시도
이번에는 피어 노드와 연결이 됨과 동시에 goroutine으로 메시지를 읽어 들여서 buffered 채널에 저장하여 필요할 때 가져와 쓸 수 있도록 코드를 수정해 보았습니다.
readMessages 메서드는 다음과 같이 작성하였습니다.
func (sn *SimpleNode) readMessages() {
for {
select {
case <-sn.serverCloseChan:
return
default:
data, err := sn.ReadAll()
if err != nil {
if sn.Logging {
log.Printf("Error: %s\n", err)
}
continue
}
buf := bytes.NewBuffer(data)
for buf.Len() > 0 {
envelope, read, err := ParseNetworkEnvelope(buf.Bytes()) // 버퍼에서 네트워크 메시지를 읽어옴
if err != nil {
if sn.Logging {
log.Printf("Error: %s\n", err)
}
break
}
buf.Next(read) // 버퍼에서 읽어온 데이터를 제거
sn.envelopes <- envelope // envelopes 채널에 읽어온 데이터를 전송
}
}
}
}
이렇게 수정을 하고 다시 실행을 해보니 이번에는 남는 데이터가 생기더라도 정상적으로 네트워크 메시지로 파싱 하는 것을 확인할 수 있었습니다.
🔍 12장 블룸필터로 관심 트랜잭션 가져오기
문제는 새로운 국면을 맞이하게 됩니다. filterload 메시지를 보내고 getheaders 메시지를 보내면 피어 노드에서 연결을 끊어버립니다. 이유도 알려주지 않고 이러는 거 너무 냉혹한 거 아닌가요?
getheaders 메시지를 보내지 않고 filterload 메시지만 보낸 경우에도 동일합니다.
filterload 메시지를 보내지 않고 getheaders 메시지만 보내면 블록 헤더 응답을 검증할 때와 마찬가지로 정상적으로 응답을 받을 수 있습니다. 그런데 이번에는 getdata 메시지를 보내고 응답을 받을 수 없는 문제가 발생합니다. 아무래도 filterload 메시지와 getdata 메시지를 중점적으로 살펴봐야 할 것 같습니다.
원인은 filterload 메시지의 match item flag를 리틀 엔디언으로 변환하지 않고 단순 바이트 값으로 0x00 또는 0x01을 집어넣은데 있었습니다. 코드는 아래와 같이 리틀 엔디언으로 변환하도록 수정했습니다.
그런데 이렇게 수정을 하고 filterload 메시지를 직렬화한 결과가 파이썬 예제와 동일한 것까지 확인을 했는데도 동일하게 피어 노드에서 연결을 끊어버리는 문제가 발생했습니다.
파이썬으로 실행해도 동일하게 filterload 메시지와 getheaders 메시지를 보내고 나면 피어 노드에서 연결을 끊어버리는 것 같습니다.
혹시 몰라서 다른 테스트넷 노드의 주소를 찾아서 실행을 해보니 이번에는 filterload 메시지를 보내고 getheaders 메시지를 보낸 다음에 headers 메시지 응답을 받는 것까지 잘 됩니다!
왜 어떤 노드랑 연결하면 되고 어떤 노드는 안되는지 생각을 조금 해보자면, 예제 코드에서는 저자가 직접 세팅해 놓고 직접 띄워놓은 노드와 연결하여 메시지를 주고받기 때문에 버전을 일일이 확인할 필요도 사용가능한 명령어를 확인할 필요도 없었습니다.
그러나 현재는 해당 노드들이 다운되어 있어서 연결이 불가능하기 때문에 저는 비트코인 네트워크 상에 존재하는 임의의 노드와 연결을 시도했고, 해당 노드들이 어떤 버전을 사용하고 어떤 명령어가 사용가능한지 알 수 없기 때문에 별도의 버전 확인이나 사용가능한 명령어 확인이 필요했던 것입니다. 비트코인 네트워크에 대한 이해도가 아직 부족한지라 추측성 판단이기는 하나, 뭐가 문제였는지 조금은 이해하게 되어서 후련한 것 같습니다. 무지성 따라 치기의 폐해라고도 할까요...
filerload 메시지는 이렇게 해결이 되었고 다음으로 getdata 메시지를 보낸 것에 대한 응답으로 merkleblock 메시지와 tx 메시지를 받아서 관심 트랜잭션이 들어있는지 확인해야 하는데 다음과 같이 잘못된 네트워크 매직이라는 오류가 연속해서 발생합니다.
데이터를 읽는 과정에서 버퍼 크기 때문에 끊긴 상태로 읽어 들인 것이 그대로 머클블록으로 파싱 되는 사고가 문제를 발견하였습니다.
🧟♂️ 4차 시도
이제는 깨달았기 때문에 바로 해결해 보겠습니다. 읽어 들인 모든 바이트 스트림을 저장하는 data 버퍼를 선언한 다음 goroutine 안에서는 data 버퍼로부터 데이터를 읽어 메시지로 파싱 하여 채널에 넣고, for문 안에서는 피어로부터 전달된 데이터를 읽어 data 버퍼에 쓰도록 코드를 수정했습니다. 이때 data 버퍼에 동시에 접근하여 race 컨디션이 발생하는 것을 방지하기 위해 mutex를 사용하여 동시 접근을 제어했습니다.
func (sn *SimpleNode) readMessages() {
mu := sync.Mutex{} // data 버퍼에 동시에 접근하는 것을 방지하기 위해 사용
data := new(bytes.Buffer) // 읽어온 데이터를 저장하는 버퍼
go func() {
for {
select {
case <-sn.serverCloseChan:
data.Reset()
return
default:
mu.Lock()
if data.Len() < 24 { // 4 + 12 + 4 + 4 (magic + command + payload length + checksum) (최소한의 길이를 만족하지 못하면 메시지를 읽어올 수 없음)
mu.Unlock()
time.Sleep(100 * time.Millisecond)
continue
}
b := data.Bytes() // 버퍼의 데이터를 읽어옴
payloadLength := utils.LittleEndianToInt(b[16:20]) // 페이로드 길이를 읽어옴
totalLength := 4 + 12 + 4 + payloadLength + 4 // magic + command + payload length + payload + checksum (메시지의 전체 길이)
if len(b) < totalLength {
mu.Unlock()
time.Sleep(100 * time.Millisecond)
continue
}
rawMsg := make([]byte, totalLength)
copy(rawMsg, b[:totalLength])
data.Next(totalLength) // 버퍼에서 읽어온 데이터를 제거
mu.Unlock()
envelope, _, err := ParseNetworkEnvelope(rawMsg) // rawMsg를 NetworkEnvelope로 변환
if err != nil {
if sn.Logging {
log.Printf("Error: %s\n", err)
}
continue
}
sn.envelopes <- envelope // envelopes 채널에 읽어온 데이터를 전송
}
}
}()
for {
buf := make([]byte, 1024) // 1KB 버퍼를 사용
select {
case <-sn.serverCloseChan:
return
default:
n, err := sn.conn.Read(buf) // n은 읽어온 데이터의 길이
if err != nil {
if err == io.EOF {
if sn.Logging {
log.Println("Connection closed by peer")
}
return
}
if sn.Logging {
log.Printf("Error: %s\n", err)
}
continue
}
mu.Lock()
_, err = data.Write(buf[:n])
if err != nil {
if sn.Logging {
log.Printf("Error: %s\n", err)
}
mu.Unlock()
continue
}
mu.Unlock()
}
}
}
짠! 정상적으로 merkleblock 메시지가 파싱 되는 것을 확인할 수 있습니다.
노드의 로그를 잠시 끄고 결과를 확인하면 다음과 같습니다. 관심 트랜잭션을 정상적으로 찾은 것을 확인할 수 있습니다.
📒 연습문제 6번
마지막으로 연습문제 6번만 풀고 마무리하겠습니다. 다음과 같은 절차로 코드를 실행해야 합니다.
- 7장의 마지막 연습문제에서 테스트넷 주소를 생성하고 faucet을 받아서 트랜잭션을 생성해 보았습니다. 이를 참고해서 생성한 주소를 블룸필터에 적용하여 UTXO를 찾습니다.
- 찾은 UTXO 안의 비트코인을 자신의 다른 주소로 보냅니다.
지난번 7장의 마지막 연습문제에서 트랜잭션 수수료를 계산하지 않고 트랜잭션을 생성하는 바람에 트랜잭션이 컨펌되지 않았습니다. 이번에는 제대로 생성을 해보도록 하죠.
다음의 코드를 실행합니다.
func GenTestnetTx() {
secret1 := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss rules the world"))) // 개인 키 생성
privateKey1, _ := ecc.NewS256PrivateKey(secret1.Bytes())
secret2 := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss ruins the world"))) // 개인 키 생성
privateKey2, _ := ecc.NewS256PrivateKey(secret2.Bytes())
address1 := privateKey1.Point().Address(true, true) // 비트코인을 보내는 주소
address2 := privateKey2.Point().Address(true, true) // 비트코인을 받는 주소
prevTx := "e770e0b481166da7d0d139c855e86633a12dbd4fa9b97f33a31fc9a458f8ddd7" // 이전 트랜잭션 ID (faucet -> address1)
prevIndex := 0 // 이전 트랜잭션의 출력 인덱스
txIn := tx.NewTxIn(prevTx, prevIndex, nil)
balance := 1193538 // 잔고
changeAmount := balance - (balance * 7 / 10) // 잔액
changeH160, _ := utils.DecodeBase58(address1) // 잔액을 받을 주소
changeScript := script.NewP2pkhScript(changeH160) // p2pkh 잠금 스크립트 생성
changeOutput := tx.NewTxOut(changeAmount, changeScript) // 트랜잭션 출력 생성
targetAmount := balance * 6 / 10 // 사용할 금액
targetH160, _ := utils.DecodeBase58(address2) // 받을 주소
targetScript := script.NewP2pkhScript(targetH160) // p2pkh 잠금 스크립트 생성
targetOutput := tx.NewTxOut(targetAmount, targetScript) // 트랜잭션 출력 생성
txObj := tx.NewTx(1, []*tx.TxIn{txIn}, []*tx.TxOut{changeOutput, targetOutput}, 0, true, false) // address1 -> address2로 비트코인 전송 트랜잭션 생성
ok, err := txObj.SignInput(0, privateKey1, true) // 서명 생성
if err != nil {
log.Fatal(err)
}
log.Println("Signature valid?", ok)
serializedTx, err := txObj.Serialize() // 트랜잭션 직렬화
if err != nil {
log.Fatal(err)
}
hexTx := hex.EncodeToString(serializedTx)
log.Println("Sending transaction:", hexTx)
body := bytes.NewBuffer([]byte(hexTx))
client := http.DefaultClient
req, err := http.NewRequest(http.MethodPost, "https://blockstream.info/testnet/api/tx", body) // testnet 블록체인에 트랜잭션 전송
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) // 응답 바디 읽기
if err != nil {
log.Fatal(err)
}
fmt.Println(string(respBody)) // 트랜잭션 ID 출력 - 4036b355ee1274384e397b58c3d3caff755acb20a8747c340cda29f5bd03ef24
}
실행 결과는 다음과 같습니다.
$ go run .
2024/01/17 14:13:37 Signature valid? true
2024/01/17 14:13:37 Sending transaction: 0100000001d7ddf858a4c91fa3337fb9a94fbd2da13366e855c839d1d0a76d1681b4e070e7000000006a47304402200b0a3e9be0774fc0da3cf5b87e437c1cafd8ec6c836b46b2254161292bd9fa70022076cd5284d52166f8542e7287a7f67322364e8a083bdb8276d26dd70b365653cb012103a7005a25ae9cf0ed9804d4d5f1b0bea6d7b8e901dd4bfa4e21d0914b7e195d74ffffffff02ae760500000000001976a914bb55f73b3c61e3c4e45bf2466a67109652cde9bf88ac5aed0a00000000001976a9146a22ffe34922ace2542d53e06a05bb275033060188ac00000000
2024/01/17 14:13:37 Tx: tx: 4036b355ee1274384e397b58c3d3caff755acb20a8747c340cda29f5bd03ef24
4036b355ee1274384e397b58c3d3caff755acb20a8747c340cda29f5bd03ef24
출력된 트랜잭션 id를 블록 탐색기에서 검색해 보면 이번에는 컨펌된 것을 확인할 수 있습니다.
블룸필터를 적용하여 UTXO를 찾고 트랜잭션을 생성하는 과정은 다음과 같습니다.
func Practice6() {
secret1 := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss rules the world"))) // 개인 키 생성
privateKey1, _ := ecc.NewS256PrivateKey(secret1.Bytes())
secret2 := utils.LittleEndianToBigInt(utils.Hash256(utils.StringToBytes("piatoss ruins the world"))) // 개인 키 생성
privateKey2, _ := ecc.NewS256PrivateKey(secret2.Bytes())
address1 := privateKey1.Point().Address(true, true) // 비트코인을 보내는 주소
address2 := privateKey2.Point().Address(true, true) // 비트코인을 받는 주소
h160, _ := utils.DecodeBase58(address1) // address1의 공개 키 해시
node, err := network.NewSimpleNode("84.250.85.135", 18333, network.TestNet, false) // testnet 노드에 연결
if err != nil {
log.Fatal(err)
}
defer node.Close()
log.Println("Connected to", node.Host, "on port", node.Port)
bf := bloomfilter.New(30, 5, 90210) // 블룸 필터 생성
bf.Add(h160) // 블룸 필터에 공개 키 해시 추가
resp, err := node.HandShake() // 핸드셰이크
if err != nil {
log.Fatal(err)
}
if ok := <-resp; !ok { // 핸드셰이크 실패 시 종료
log.Fatal("Handshake failed")
}
time.Sleep(1 * time.Second)
log.Println("Sending filterload message")
if err := node.Send(bf.Filterload()); err != nil { // 블룸 필터로 필터로드 메시지 전송
log.Fatal(err)
}
log.Println("Sending getheaders message")
getheaders := network.DefaultGetHeadersMessage() // getheaders 메시지 생성
startBlock, _ := hex.DecodeString("0000000000000014fbf5791d8333ef9cea0c87aff27f98e102dbcf40963aea8b") // 2573311번 블록 (address1 -> address2로 비트코인 전송한 블록은 2573313번 블록)
getheaders.StartBlock = startBlock // getheaders 메시지에 start_block 필드 설정
if err := node.Send(getheaders); err != nil { // getheaders 메시지 전송
log.Fatal(err)
}
done := make(chan struct{})
envelopes, errs := node.WaitFor([]network.Command{network.HeadersCommand}, done) // headers 메시지 대기
getdata := network.NewGetDataMessage() // getdata 메시지 생성
go func() {
defer close(done)
for {
select {
case err := <-errs:
if err == io.EOF { // 에러가 EOF일 경우 종료
log.Println("Connection closed")
return
}
log.Fatalf("Error receiving message: %s", err)
case headersEnvelope := <-envelopes:
if headersEnvelope == nil {
continue
}
headers, err := network.ParseHeadersMessage(headersEnvelope.Payload) // headers 메시지 파싱
if err != nil {
log.Fatal(err)
}
log.Printf("Received headers message with %d headers\n", len(headers.Headers))
// 블록 헤더 검증
for _, header := range headers.Headers {
ok, err := header.CheckProofOfWork() // 작업 증명 검증
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("Block does not satisfy proof of work")
}
hash, err := header.Hash() // 블록 해시
if err != nil {
log.Fatal(err)
}
getdata.AddData(network.FiltedBlockDataItem, hash) // getdata 메시지에 필터링된 블록 데이터 추가
}
return
}
}
}()
<-done
time.Sleep(1 * time.Second)
log.Println("Sending getdata message")
if err := node.Send(getdata); err != nil { // getdata 메시지 전송
log.Fatal(err)
}
done = make(chan struct{})
envelopes, errs = node.WaitFor([]network.Command{network.MerkleBlockCommand, network.TxCommand}, done) // merkleblock, tx 메시지 대기
go func() {
defer close(done)
for {
select {
case err := <-errs:
if err == io.EOF { // 에러가 EOF일 경우 종료
log.Println("Connection closed")
return
}
log.Fatalf("Error receiving message: %s", err)
case envelope := <-envelopes:
switch envelope.Command.String() {
case network.MerkleBlockCommand.String():
mb := merkleblock.MerkleBlock{}
err := mb.Parse(envelope.Payload) // merkleblock 메시지 파싱
if err != nil {
log.Fatalf("Error parsing merkle block: %s", err)
}
ok, err := mb.IsValid() // merkleblock 메시지 검증
if err != nil {
log.Fatalf("Error validating merkle block: %s", err)
}
if !ok {
log.Fatal("Merkle block is not valid")
}
case network.TxCommand.String():
transaction, err := tx.ParseTx(envelope.Payload) // tx 메시지 파싱
if err != nil {
log.Fatalf("Error parsing tx: %s", err)
}
for i, out := range transaction.Outputs { // tx 메시지의 출력 검색
if strings.EqualFold(out.ScriptPubKey.Address(true), address1) {
prevTx, _ := transaction.ID()
log.Printf("Found matching tx %s at output index %d\n", prevTx, i)
prevIndex := 0 // 이전 트랜잭션의 출력 인덱스
txIn := tx.NewTxIn(prevTx, prevIndex, nil) // 이전 트랜잭션의 출력을 참조하는 트랜잭션 입력 생성
balance := 358062 // 잔고 (하드코딩)
changeAmount := balance - (balance * 7 / 10) // 잔액
changeH160, _ := utils.DecodeBase58(address1) // 잔액을 받을 주소
changeScript := script.NewP2pkhScript(changeH160) // p2pkh 잠금 스크립트 생성
changeOutput := tx.NewTxOut(changeAmount, changeScript) // 트랜잭션 출력 생성
targetAmount := balance * 6 / 10 // 사용할 금액
targetH160, _ := utils.DecodeBase58(address2) // 받을 주소
targetScript := script.NewP2pkhScript(targetH160) // p2pkh 잠금 스크립트 생성
targetOutput := tx.NewTxOut(targetAmount, targetScript) // 트랜잭션 출력 생성
txObj := tx.NewTx(1, []*tx.TxIn{txIn}, []*tx.TxOut{changeOutput, targetOutput}, 0, true, false) // address1 -> address2로 비트코인 전송 트랜잭션 생성
ok, err := txObj.SignInput(0, privateKey1, true) // 서명 생성
if err != nil {
log.Fatal(err)
}
log.Println("Signature valid?", ok)
serializedTx, err := txObj.Serialize() // 트랜잭션 직렬화
if err != nil {
log.Fatal(err)
}
hexTx := hex.EncodeToString(serializedTx)
log.Println("Sending transaction:", hexTx)
body := bytes.NewBuffer([]byte(hexTx))
client := http.DefaultClient
req, err := http.NewRequest(http.MethodPost, "https://blockstream.info/testnet/api/tx", body) // testnet 블록체인에 트랜잭션 전송
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) // 응답 바디 읽기
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction ID:", string(respBody)) // 트랜잭션 ID 출력
return
}
}
}
}
}
}()
<-done
log.Println("Done")
}
실행 결과는 다음과 같습니다.
$ go run .
2024/01/17 14:33:31 Connected to 84.250.85.135 on port 18333
2024/01/17 14:33:33 Sending filterload message
2024/01/17 14:33:33 Sending getheaders message
2024/01/17 14:33:33 Received headers message with 40 headers
2024/01/17 14:33:34 Sending getdata message
2024/01/17 14:33:34 Found matching tx 4036b355ee1274384e397b58c3d3caff755acb20a8747c340cda29f5bd03ef24 at output index 0
2024/01/17 14:33:35 Signature valid? true
2024/01/17 14:33:35 Sending transaction: 010000000124ef03bdf529da0c347c74a820cb5a75ffcad3c3587b394e387412ee55b33640000000006a47304402206c7b8cb081d2a1c5ca6d703cee48a0a7b8f45a95c91172cba6164e4e21f2d5fc02205d536053d44fe5aff4289f5a34b2311747b0537d062ab7b5760327586a8c36a1012103a7005a25ae9cf0ed9804d4d5f1b0bea6d7b8e901dd4bfa4e21d0914b7e195d74ffffffff029ba30100000000001976a914bb55f73b3c61e3c4e45bf2466a67109652cde9bf88ac35470300000000001976a9146a22ffe34922ace2542d53e06a05bb275033060188ac00000000
Transaction ID: 35703df31ee3dec4087138e76cf53dfd4cb2070d9899e30cf13aef508fb3bcd7
블록 탐색기에서 트랜잭션이 컨펌된 것을 확인할 수 있습니다.
이로써! <외전1 네트워크 요청 및 응답 개선>는 마무리되었습니다. 중요한 것은 꺾이지 않는 마음. 긴 글 읽어주셔서 감사합니다.
📖 참고자료
'블록체인 > Bitcoin' 카테고리의 다른 글
밑바닥부터 시작하는 비트코인 - 외전2 비트코인 백서 의식의 흐름대로 읽기 (0) | 2024.01.19 |
---|---|
밑바닥부터 시작하는 비트코인 - 13장 세그윗 2 (1) | 2024.01.14 |
밑바닥부터 시작하는 비트코인 - 13장 세그윗 1 (0) | 2024.01.13 |
밑바닥부터 시작하는 비트코인 - 12장 블룸 필터 (1) | 2024.01.10 |
밑바닥부터 시작하는 비트코인 - 11장 단순 지급 검증 (1) | 2024.01.09 |