파이썬으로 배우는 블록체인 구조와 이론-6장 비트코인 P2P 프로토콜(2)

ImHyunbin
Quantum Ant
Published in
16 min readAug 12, 2019

04.신규 블록 데이터 릴레이

채굴자가 새로운 블록을 생성하면 풀 노드에게 전달하고, 풀 노드는 이 블록을 검증해 또 다른 풀 노드에 전파한다. 이때 블록을 주고받는 절차가 필요하다.

이전 방식의 블록 전송

노드 A가 새로운 블록을 받으면 이 블록을 노드 B에게 전파한다.

이전의 블록 릴레이 절차

1. 노드 A가 새로운 블록을 받으면 이를 검증한다. 새로운 블록을 보낸 곳은 채굴자일 수도 있고, 다른 풀 노드일 수도 있다.

2. 블록 검증이 완료되면 노드 A는 새로운 블록을 가지고 있다는 의미로 headers나 inv 메시지를 노드 B에게 보낸다.

이 메시지에는 블록 헤더의 해시 값을 기록하고, inv의 경우 타입(type)에는 ‘MSG_BLOCK’을 기록해 인벤토리(inventory:inv)가 블록에 대한 것임을 표시한다.

3. 노드 B는 해당 블록이 없으면 getdata를 통해 그 블록을 보내 달라고 요청한다.

getdata 메시지에도 요청할 블록 헤더의 해시 값을 기록하고, 타입도 ‘MSG_BLOCK’으로 기록한다.

4. 노드 A는 해당 블록 데이터를 B에게 전송한다.

현재 방식의 블록 전송

이전 방식의 블록 전송의 효율이 낮아, 2016년 8월 BIP-152로 블록 전송 기능이 변경됐다.

기본 아이디어는 블록 데이터에서 노드 B가 이미 가지고 있는 트랜잭션들은 보낼 필요가 없다는 것이다. 노드 B도 평소에 트랜잭션들을 검증하고 이를 자신의 메모리풀(mempool)에 저장하고 있다. 이것을 이용하면 블록에 있는 트랜잭션을 모두 보낼 필요가 없으므로 데이터 전송량을 상당히 줄일 수 있다.

이전의 블록 릴레이 방식

저대역폭 릴레이(Low Bandwidth Relaying)

노드 A가 신규 블록을 완전히 검증한 후에 노드 B에게 보내는 절차다.

노드 B는 신규 블록의 헤더와 트랜잭션 ID만 받고, 자신의 메모리풀에 저장된 트랜잭션을 이용해 이 블록을 조립할 수 있다. 자신의 메모리 풀에 없는 트랜잭션은 추가로 A에게 요청해서 전달받는다.

Low Bandwidth Relaying

1. 노드 B는 사전에 노드 A에게 sendcmpct 메시지를 보내서 블록 데이터를 보낼 때 콤팩트(compact) 형식으로 보내 달라고 요청해 둔다. 이때 sendcmpct 메시지에는 저대역폭 절차를 따른다는 의미로 ‘0’을 기록한다.

2. 노드 A가 신규 블록을 받으면 이를 검증한다.

3. 노드 A는 headers나 inv 메시지로 새로운 블록이 생겼다는 것을 노드 B에게 알려준다.

4. 노드 B는 A에게 getdata 메시지를 보내서 블록 데이터를 보내 달라고 요청한다. 이때 getdata에는 콤팩트 형식으로 보내 달라는 의미로 ‘CMPCT’를 기록한다.

5. 노드 A는 B에게 cmpctblock 메시지를 보내 콤팩트 형식으로 블록 데이터를 보낸다. 콤팩트 형식에는 블록 헤더와 트랜잭션 ID, 그리고 몇 개의 관리용 정보가 들어 있다.

6. 노드 B는 콤팩트 블록 데이터와 자신의 메모리풀에 있는 트랜잭션으로 해당 블록을 조립한다. 이때 해당 블록이 자신의 메모리풀에 없는 트랜잭션을 포함하고 있으면 getblocktxn 메시지를 보내 이를 보내 달라고 요청한다.

7. 노드 A는 blocktxn 메시지를 통해 B가 원하는 트랜잭션 데이터를 보낸다.

고대역폭 릴레이(High Bandwidth Relaying)

노드 A가 신규 블록을 검증하기 전에 노드 B에게 보내는 절차다.

이 절차의 기본 아이디어는 노드 B도 블록을 검증할 것이므로 A가 굳이 검증 후에 보낼 필요가 없다는 것이다.

High Bandwidth Relaying

노드 B는 사전에 노드 A에게 sendcmpct 메시지를 보내서 신규 블록이 발생하면 headers나 inv 절차 없이 곧바로 콤팩트(compact) 형식의 블록을 보내 달라고 요청해 둔다. 이때 sendcmpct 메시지에는 고대역폭 절차를 따른다는 의미로 ‘1’을 기록한다.

2. 노드 A는 블록을 검증하기 전에 노드 B에게 cmpctblock 메시지로 콤팩트 블록을 보낸다.

3. 노드 B는 콤팩트 블록 데이터와 자신의 메모리풀에 있는 트랜잭션으로 해당 블록을 조립한다. 이때 해당 브록이 자신의 메모리풀에 없는 트랜잭션을 포함하고 있으면 getblocktxn 메시지를 보내 이를 보내 달라고 요청한다.

4. 노드 A는 blocktxn 메시지를 통해 B가 원하는 트랜잭션 데이터를 보낸다.

05.트랜잭션 릴레이

새로운 트랜잭션이 발생하면 각 노드들은 이를 검증하고 인근 노드로 전파한다. 트랜잭션 데이터도 inv, getdata, tx 메시지를 통해 이뤄진다. 이때 데이터 타입은 MSG_TX나 MSG_WITNESS다.

트랜잭션 전파 절차

1. 노드 A는 누군가로부터 새로운 트랜잭션(tx)을 받았다. A는 이 트랜잭션을 검증하고 inv 메시지를 통해 자신이 새로운 트랜잭션을 가지고 있다는 사실을 노드 B에게 알린다.

inv 메시지에는 이것이 트랜잭션이라는 것을 표시하기 위해 MSG_TX나 MSG_WITHNESS를 기록한다. (MSG_TX는 세그윗 이전의 트랜잭션 형식이고 MSG_WITHNESS는 세그윗 기능 이후의 형식이다.)

2. 노드 B는 inv 메시지에 있는 트랜잭션 해시 값을 보고 자신의 메모리풀에 없는 트랜잭션이면 getdata 메시지를 통해 이를 요청한다. 이 트랜잭션이 이미 메모리풀에 있으면 getdata를 보낼 필요가 없다.

3. 노드 A는 B가 요청한 트랜잭션을 보낸다.

06.기타 메시지

reject 메시지

노드들은 트랜잭션이나 블록을 받으면 데이터의 유효성을 검증하고 다른 노드로 전파하는데, 검증 과정에서 잘못된 내용이 발견되면 발신자 노드에 reject 메시지를 통해 무엇이 문제인지 알려준다.

reject를 받은 발신자 노드는 이 정보를 참고용으로만 사용

특정 노드가 악의적인 목적으로 모든 정상 데이터에 대해 reject를 보낼 수도 있으므로 reject를 받은 노드는 자기가 보낸 데이터를 재확인하거나, reject를 무시하는 등으로 처리한다.

feefilter 메시지

feefilter는 수수료가 얼마 이상인 트랜잭션만 보내 달라고 요청할 때 사용

스팸 트랜잭션을 방지할 때 사용될 수도 있고, 채굴자가 트랜잭션을 승인할 때도 사용될 수 있다.

mempool 메시지

mempool 메시지는 아직 채굴되지 않고 메모리풀에 쌓여 있는 트랜잭션을 보내 달라고 요청할 때 사용

어떤 노드가 네트워크에 처음 접속할 때 유용하게 사용될 수 있고, 채굴자들이트랜잭션을 모을 때도 유용하게 사용될 수 있다.

notfound 메시지

notfound 메시지는 getdata의 응답으로 사용

getdata로 요청한 트랜잭션이나 블록 데이터, 혹은 머클 블록이 메모리풀에 없으면 notfound 메시지로 응답한다.

패널티 부여 및 노드 차단

특정 노드가 악의적인 목적이나 실수로 잘못된 메시지를 보내면 이를 받은 노드는 해당 노드에 벌점(penalty)을 부여한다. 이것은 특정 노드의 부정행위로부터(DoS 공격 등) 네트워크를 보호하려는 조치다.

벌점은 메시지의 중요도에 따라 차등 적용된다.

07.SPV 노드-Simplified Payment Verification

풀 노드는 블록체인 데이터 전체를 가지고 있지만 SPV 노드는 블록의 헤더만 가지고 있다.

블록체인 데이터는 2019년 1월 기준 약 230GB로 저장 공간이 큰 장비에서 운용할 수 있다. 반면 SPV노드는 저장 공간이 작은 스마트 기기 같은 소형 장비에 적합하다.

블록 데이터는 1MB 이상인 반면 헤더 데이터는 80바이트이므로 저장 공간을 대폭 줄일 수 있다.

스마트 기기의 지갑 애플리케이션은 대부분 SPV 노드로 동작한다.

풀노드는 모든 트랜잭션을 관리할 수 있지만 SPV노드는 자신과 관련된 트랜잭션만 관리한다. 자신의 트랜잭션이 승인되어 블록에 포함됐는지는 풀 노드의 도움을 받아야 확인할 수 있다.

확인 절차

1. SPV 노드가 트랜잭션을 송출하고, 트랜잭션은 네트워크에 전파된다. SPV 노드는 자신이보낸 트랜잭션의 해시값(ID)을 알고 있다.

2. SPV 자신이 보낸 트랜잭션이 다시 자신에게 돌아오면 정상적으로 전파됐는지 확인할 수 있다.

3.채굴자가 새로운 블록을 생성하면 SPV 노드는 이 블록의 헤더를 받는다. SPV는 항상 풀 노드와 블록 헤더를 동기화시키기 때문에 새로 생성된 블록의 헤더를 받을 수 있다.

4. SPV는 이 블록에 자신이 송출한 트랜잭션이 포함됐는지 확인하기 위해, 풀 노드에게 이 블록에 자신과 관련된 트랜잭션이 있으면 보내 달라고 요청한다. 이때 SPV는 자신의 IP와 트랜잭션에 기록된 공개키 정보 등이 풀 노드에게 노출되는 위험이 발생한다.

5. SPV는 자신의 정보를 최대한 숨기기 위해 블룸 필터(bloom filter)라는 것을 이용해서 자신과 관련된 트랜잭션뿐만 아니라, 자신과 관련 없는 것들도 섞어서 풀 노드에게 요청한다. 그러면 풀 노드는 여러 개의 트랜잭션 중 어느 것이 SPV 것인지 확실히 알 수는 없다. 이로 인해 위험이 다소 줄어든다.

6. 풀 노드는 SPV가 요청한 트랜잭션들과 그 트랜잭션과 관련된 부분의 머클 트리 정보를 보낸다.

7. SPV는 풀 노드가 보낸 트랜잭션들 중에 자신의 트랜잭션이 있는지 확인하고, 풀 노드가 보낸 부분적인 머클 트리를 이용해 머클 루트를 계산한다.

8. SPV는 자신이 가지고 있는 헤더에 기록된 머클 루트와 풀 노드가 보낸 정보로 계산한 머클 루트가 일치하는지 확인한다. 일치하면 자신의 트랜잭션이 이 블록에 포함됐다는 것이 확인된 것이다.

9. SPV는 이후에 생성되는 블록 헤더를 알 수 있으므로, 자신의 트랜잭션이 기록된 블록 이후에 몇 개의 블록이 추가되는지 블록 깊이(block depth)를 확인할 수 있다. 6개 이상이면 자신의 트랜잭션이 최종 승인된 것이다.

위 절차는 SPV가 송금한 것을 기준으로 했으나, 수금하는 절차도 원리는 같다.

SPV 노드는 블록체인 데이터를 가지고 있지 않기 때문에 절차가 다소 복잡하고 위험도 존재한다. 이것은 블록체인 데이터가 없는 편리성에 대한 대가로 생각할 수 있다.

블룸 필터(Bloom Filter)

블룸 필터는 어떤 원소가 특정 집합에 속해 있는지만 확인할 때 사용되는 유용한 자료 구조다.

블룸 필터 (출처 : https://en.wikipedia.org/wiki/Bloom_filter#/media/File:Bloom_filter.svg)

블룸 필터의 최적 비트 수 m과 해시 함수의 개수 k는 다음 공식으로 결정한다. N은 등록할 단어의 개수이고, p는 희망하는 False positive error이다.

(출처 : https://en.wikipedia.org/wiki/Bloom_filter)

SPV와 블룸 필터

SPV 노드는 블룸 필터를 이용해 자신과 관련된 트랜잭션을 포함한 여러 개의 트랜잭션과 머글 트리 정보를 이용해 자신의 트랜잭션이 승인됐는지 확인한다.

블룸 필터를 사용하면 SPV 노드는 자신의 정보가 직접 노출되는 것을 방지할 수 있다.

풀 노드는 블룸 필터를 통과하는 트랜잭션을 모두 보내주므로 그중 어느 것이 SPV 노드에 속하는지 판단하기 어렵다.

SPV 블룸 필터 절차

1. SPV가 네트워크에 접속하면 풀 노드에게 version 메시지를 보낸다. 이때 version에 있는 relay 플래그를 False로 설정한다. 이는 filterload 메시지를 보낼 때까지는 트랜잭션을 보내지 말라는 의미다.

2. SPV는 원하는 트랜잭션만 받기 위해 블룸 필터를 만들고 filterload 메시지로 필터와 사용할 해시 함수의 개수 등을 풀 노드에 보낸다.

3. 풀 노드는 트랜잭션을 전파할 때 필터를 통과한 트랜잭션의 해시 값을 inv에 기록해서 SPV에 보낸다.

4. SPV는 inv에 기록된 트랜잭션을 보내 달라고 풀 노드에게 getdata 메시지를 보낸다.

5. 풀 노드는 tx 메시지로 SPV가 요청하는 트랜잭션을 보낸다. 여기까지 절차로 SPV는 자신과 관련된 트랜잭션과 기타 필터를 통과한 트랜잭션들을 받을 수 있다.

6. 한편 평소에 SPV는 블록 헤더를 동기화한다. 풀 노드는 inv 메시지로 새로운 블록이 생성됐다고 SPV에 알려준다.

7. SPV는 해당 블록의 헤더가 없으면 getheaders 메시지를 보내 헤더를 보내 달라고 요청한다.

8. 풀 노드는 headers 메시지로 SPV에 블록 헤더를 보낸다. 여기까지 절차로 SPV는 블록의 헤더를 모두 가지고 있다.

9. SPV가 트랜잭션을 송출한 상태이고, (8)번 절차로 새로운 블록 헤더를 받은 상태다. SPV는 이 블록 안에 자신의 트랜잭션이 포함됐는지 확인하고 싶다. SPV는 플 노드에 getdata 메시지를 보내 이 블록의 바디에 기록된 트랜잭션 중 필터를 통과하는 것을 보내 달라고 요청한다. 이때 타입에는 MSG_FILTERED_BLOCK을 기록한다.

10. 풀 노드는 SPV가 검증할 수 있도록 merkleblock 메시지를 통해 머클 트리의 재료를 보낸다. SPV는 이 정보로 머클 트리를 구축할 수 있고 머클 루트를 계산할 수 있다.

11. 풀 노드는 merkleblock에 이어 이 블록에 포함된 트랜잭션 중 필터를 통과한 것들을 tx 메시지로 SPV에 보낸다. SPV는 여기서 받은 tx와 (10)번 절차에서 계산한 머클 루트로 자신의 트랜잭션이 승인됐는지(이 블록에 포함됐는지) 확인할 수 있다. 가지고 있는 헤더의 머클 루트와 계산한 머클 루트가 일치하면 이 트랜잭션은 이 블록에 있는 것이 분명하다.

12. 도중에 필터를 변경할 때는 filteradd 메시지를 보낸다.

13. 필터 절차를 종료하고 싶을 때는 filterclear 메시지를 보낸다.

필터로드(filterload) 메시지

filterload 메시지 구조(출처 : https://bitcoin.org/en/developer-reference#filterload)
nFlags filed(출처 : https://bitcoin.org/en/developer-reference#filterload)

nFilterBytes는 블룸 필터의 바이트 크기이고 filter는 트랜잭션을 필터링할 실제 블룸 필터다.

nHashFuncs은 사용할 해시 함수의 개수다.

SPV는 자신이 받고 싶은 트랜잭션에 포함될 정보를 미리 필터에 등록한다.

필터의 대상은 스크립트(자신의 공개키를 포함한 언로킹, 로킹 스크립트), txid, utxo 등이다. 그리고 자신을 숨기기 위해 임의의 비트를 추가로 설정한다. 이 필터를 받은 풀 노드는 트랜잭션 중 이 필터를 통과한 것만 SPV에 보낸다.

SPV가 자신을 숨기기 위해 불필요한 트랜잭션을 많이 요구할수록(loose filter) 자신의 정보가 노출되는 정도는 감소하지만 트랜잭션 전송량은 증가한다.

트랜잭션이 적을수록(strict filter) 전송량은 감소하지만, 정보 노출 정도는 증가한다.

SPV는 프라이버스(privacy)와 트래픽(traffic) 사이의 트레이드 오프 관계를 고려해 필터를 설정한다.

SPV가 nHashFuncs개로 필터를 구성했으므로 풀 노드도 트랜잭션을 검색할 때 nHashFuncs개의 해시 함수를 사용해야 한다.

머클 블록과 트랜잭션 검증

SPV 노드는 위의 SPV 블룸 필터 (10)번 절차에서 받은 머클 블록(merkle block)을 이용해서 자신의 트랜잭션이 블록에 포함됐는지 검증할 수 있다.

머클 블록에는 SPV가 머클 트리를 구성할 수 있는 재료가 들어 있다.

출처 : https://bitcoin.org/en/developer-reference#merkleblock
파셜(partial) 머클 트리

1. 트랜잭션 개수(Transaction count)를 보고 하위 리프 노드가 8개인 머클 트리를 초기화 한다. 머클 트리는 이진 트리이므로 리프 노드는 짝수 개이다.

2. 플래그(Flags) (1 0 1 1 1 0 0 0)를 보고 머클 트리의 루트 노드부터 값을 채워 나간다. 왼쪽 비트부터 오른쪽으로 한 비트씩 읽어 나간다.

비트가 1이고 현재 위치가 리프 노드가 아니면 나중에 해시 값을 계산한다.

비트가 1이고 리프 노드면 해시 값(Hash #)을 기록한다. 이것은 필터를 통과한 트랜잭션이라는 의미다.

그리고 비트가 0이고 리프 노드가 아니면 해시 값(Hash #)을 기록한다.

비트가 0이고 리프 노드인 경우도 해시 값(Hash #)을 기록한다. 단, 이것은 필터를 통과하지 않은 트랜잭션을 의미한다.

3. 플래그의 2번 비트가 0이고 현재 위치가 리프 노드가 아니므로 위의 파셜 머클 트리와 같이 Hash #1을 여기에 기록한다. 그러면 이 위치 아래에 있는 노드들은 몰라도 된다. 이 위치의 해시 값은 결정됐으므로 다음은 오른쪽 노드로 이동한다.

4. 플래그의 3번 비트가 1이고 현재 위치가 리프 노드가 아니므로 나중에 해시 값을 계산해야 한다. 다음은 왼쪽 아래 노드로 이동한다.

5. 플래그의 4번 비트가 1이고 현재 위치가 리프 노드가 아니므로 계산이 필요하다. 그 다음은 이 노드가 아직 결정되지 않았으므로 오른쪽으로 이동하지 않고 왼쪽 아래 노드로 이동한다.

6. 플래그의 5번 비트가 1이고 현재 위치가 리프 노드이므로 Hash #2를 여기에 기록한다. Hash #2 필터를 통과한 트랜잭션의 해시 값이다. 그 다음은 오른쪽 노드로 이동한다.

7. 플래그의 6번 비트가 0이고 현재 위치가 리프 노드이므로 Hash #3을 여기에 기록한다. Hash #3은 필터를 통과하지 못한 트랜잭션의 해시 값이다. 이 값은 단지 머클 트리를 만들기 위해 보낸 것이다. 즉 (5)번 위치의 해시 값을 계산하려면 이 노드의 해시 값이 필요하다. 그 다음은 (5)번 위치의 하위 노드들이 모두 처리됐으므로 오른쪽 노드인 (8)번 위치로 이동한다.

8. 플래그의 7번 비트가 0이고 현재 위치가 리프 노드가 아니므로 Hash #4를 여기에 기록한다. 플래그의 8번 비트는 패딩 비트로 이 경우는 사용되지 않는다.

위 절차가 완료되면 5번 절차에서 보류했던 해시 값을 계산할 수 있다. 아래 두 노드의 해시 값인 Hash #2와 Hash #3을 알고 있으므로 해시를 계산할 수 있다. 같은 방식으로 4번과 2번 루트 노드도 해시 값을 계산할 수 있다.

이런 과정을 거쳐 머클 트리의 루트 노드를 계산할 수 있고, SPV가 알고 있는 블록 헤더에 기록된 머클 루트와 비교할 수 있다. 두 머클 루트가 일치하면 SPV가 보낸 트랜잭션이 이 블록에 분명히 포함된 것이다.

--

--