오버레이 네트워크
•
인터넷 망은 물리적 네트워크과 논리적 네트워크로 구성된다.
•
논리적 링크에 의해 컴퓨터간의 연결을 오버레이 네트워크라 한다.
•
예를들어 한국에 있는 서버와 유럽에 있는 서버는 여러 물리적 컴퓨터의 라우팅을 통해 구성되지만, 논리적 연결을 통해 피어를 맺을 수 있다.
•
오버레이 네트워크는 네트워크 구성을 탄력적으로 할 수 있고, 이를 통해 다수의 사용자와 멀티캐스트가 가능하다는 장점이 있다.
오버레이 네트워크의 분류
•
구조화 오버레이
◦
연결 상대가 미리 정해진 상태를 의미한다.
◦
메시지 수신이 보장되며 확장성이 매우 높다.
◦
네트워크 구성의 유연성이 떨어진다.
구조화 오버레이의 예
•
비구조화 오버레이
◦
인접노드를 알아서 선택한다.
◦
토폴로지가 규정되어 있지 않다.
◦
유연성 있는 전달이 가능하다.
◦
메시지 수신을 보장하지 못한다.
◦
수퍼노드가 지역적으로 서버의 역할을 수행하기도 한다.
◦
비트코인은 비구조화 오버레이를 채택하고 있다.
◦
비트코인의 경우 노드의 수가 충분해짐에 따라 수퍼노드의 필요성이 없어졌다.
◦
대다수의 P2P 네트워크는 비구조화 오버레이를 택하고 있다.
비구조화 오버레이.
P2P(peer-to-peer) 네트워크
•
P2P 네트워크는 네트워크 참여 노드가 클라이언트와 서버의 역할을 동시에 수행한다.
◦
컴퓨터가 네트워크를 참여할 때 동등한 지위(peer)를 가지고 ‘특별한’노드는 존재하지 않는다.
◦
P2P 네트워크는 본질적으로 회복력이 있고 분산화되며 개방을 지향한다.
◦
P2P 네트워크를 가장 처음 성공시킨 사례는 비트 토렌트의 전신인 냅스터이다.
•
P2P 네트워크는 브로드캐스팅(broad casting)을 수행하는데 활용된다.
◦
자신은 자신의 피어 리스트(친구 주소록)을 가지고 있다.
◦
나를 피어로 맺고 있는 사람이 데이터를 주면, 해당 데이터를 나의 피어에게 전달한다.
◦
만약 내가 받은 데이터가 이미 전송된 적이 있으면 피어에게 전송하지 않는다.
◦
만약 내가 받은 데이터의 프로토콜이 위배되는 경우 피어에게 전송하지 않는다.
◦
원본 데이터의 내용이 조금이라도 다른 경우 이는 새로운 데이터로 인지한다.
•
P2P 네트워크는 클라이언트/서버 구조와 비교하여 다음과 같은 장점을 갖는다.
클라이언트/서버 네트워크 | P2P 네트워크 | |
데이터 가용성 | 낮다. (서버가 죽으면 데이터를 받을 수 없다.) | 높다. (피어 중 한 노드에만 데이터가 존재하여도 데이터 공유가 가능하다.) |
전송 확장성 | 낮다. (클라이언트의 요청이 많을수록 속도가 저하된다.) | 높다. (요청이 많아도 속도가 저하되지 않는다.) |
데이터 무결성 | 낮다. (서버가 데이터에 대한 소유권을 갖고 있어서 언제든 변경이 가능하다.) | 높다. (데이터의 소유권이 분산되어 무결성을 유지하기 용이하다.) |
구성 | 쉽다. (서버를 운영할 단일 주체만 존재하면 된다.) | 어렵다. (여러 노드가 참여하여야 한다.) |
•
비트코인은 P2P 네트워크를 통해 다음과 같은 역할을 수행한다.
◦
전체 비트코인 장부를 분산하여 동기화한다.
◦
트랜잭션과 블록을 피어들에게 전달한다.
◦
합의의 검증 결과를 공유한다.
•
즉, 실시간 데이터 공유를 통해 탈중앙화된 프로토콜을 가능하게 한다.
클라이언트/서버 구조와 P2P 구조
비트코인의 피어
•
비트코인 노드는 최대 125개의 피어를 유지할 수 있다.
◦
최소 8개의 아웃바운드를 확보하여야 한다.
◦
브로드캐스트 요청을 받으면 125개 중 8개의 임의의 피어에게 데이터를 전송할 의무가 있다.
이 노드의 경우 11개의 피어를 확보하고 있다.
•
피어 검색
◦
그렇다면, 누가 비트코인 풀노드인지 알 수 있을까?
◦
비트코인 코어 클라이언트는 다음과 같은 순서로 피어를 검색한다.
1.
과거에 사용했던 피어를 가져온다. 그리고 연결을 시도한다.
2.
과거의 사용했던 피어가 없거나 연결을 실패하는경우 DNS Seeging을 사용한다.
•
등록된 풀노드로에게 시드를 물어본다.
DNS 시드를 제공하는 노드 들. 다수의 비트코인 코어 개발자들이 보인다.
•
해당 풀노드는 자신이 보유한 피어를 공유해준다.
3.
1, 2가 모두 실패하는 경우
•
비트코인 코어 소프트웨어에 하드코딩된 IP 주소를 사용한다.
•
최초에는 IP주소가 직접 입력 되어있었으나, BIP115의 규칙에 따라 정의되어 있다.
BIP115이전의 IP주소들. 8333포트가 명시되어 있다.
비트코인의 피어 맺기와 유지
•
노드 리스트 공유
◦
DNS Seeding이나 하드코드된 풀노드에게 노드리스트를 문의할 때 사용한다.
1.
addr : 노드 B에게 자신의 피어정보를 보낸다.
2.
getaddr : 노드 B에게 알고 있는 노드의 주소를 요청한다.
3.
addr : 노드 B는 자신이 알고 있으면서 3시간 내에 활동기록이 있는 최대 1000개의 노드를 알려준다.
•
피어 연결
◦
노드의 주소를 받았다면, 해당 노드와 피어를 맺는다.
1.
version : 프로토콜의 버전을 보낸다. 이 때, 자신이 가지고 있는 블록의 높이를 함께 알려준다.
2.
verack : 잘 받았다는 메시지를 준다.
•
피어 확인
◦
자신이 맺고 있는 피어들이 아직 잘 연결되어 있는지 확인한다.
◦
30분 간격으로 피어들에게 ping을 보낸다.
◦
랜덤 논스는 이 ping의 식별자이다.
◦
ping를 받은 피어는 pong을 보내야 한다.
◦
90분 내에 pong을 받지 못하면 피어를 끊고 연결을 종료한다.
네트워크 메시지
•
노드간의 모든 메시지는 동일한 형식을 갖는다.
◦
network magic은 이 메시지가 비트코인 메시지라는 것을 의미한다.
◦
command는 이 메시지가 어떤 종류의 메시지라는 것을 의미한다.
▪
예를 들어 메시지가 ‘version’이면 version의 아스키 코드를 적고 남는 공간을 0으로 채운다.
◦
payload는 메시지 내용을 의미한다.
▪
payload의 최대 길이는 32MB로 제한된다.
•
메시지 종류는 아래 문서에서 확인할 수 있다.
•
메시지를 보내기 위한 코드의 예시는 다음과 같다.
NETWORK_MAGIC = b'\xf9\xbe\xb4\xd9'
TESTNET_NETWORK_MAGIC = b'\x0b\x11\x09\x07'
class NetworkEnvelope:
def __init__(self, command, payload, testnet=False):
self.command = command
self.payload = payload
if testnet:
self.magic = TESTNET_NETWORK_MAGIC
else:
self.magic = NETWORK_MAGIC
def __repr__(self):
return '{}: {}'.format(
self.command.decode('ascii'),
self.payload.hex(),
)
@classmethod
def parse(cls, s, testnet=False):
'''Takes a stream and creates a NetworkEnvelope'''
# network magic
magic = s.read(4)
if magic == b'':
raise RuntimeError('Connection reset!')
if testnet:
expected_magic = TESTNET_NETWORK_MAGIC
else:
expected_magic = NETWORK_MAGIC
if magic != expected_magic:
raise RuntimeError('magic is not right {} vs {}'.format(magic.hex(), expected_magic.hex()))
# command
command = s.read(12)
command = command.strip(b'\x00')
# payload의 길이
payload_length = little_endian_to_int(s.read(4))
# 체크섬
checksum = s.read(4)
# payload 파싱
payload = s.read(payload_length)
# 체크섬 확인
calculated_checksum = hash256(payload)[:4]
if calculated_checksum != checksum:
raise RuntimeError('checksum does not match')
return cls(command, payload, testnet=testnet)
def serialize(self):
result = self.magic
result += self.command + b'\x00' * (12 - len(self.command))
result += int_to_little_endian(len(self.payload), 4)
result += hash256(self.payload)[:4]
result += self.payload
return result
def stream(self):
return BytesIO(self.payload)
Python
복사
블록 헤더 교환
•
풀노드가 최초로 피어를 맺었다면, 피어에게 블록 정보를 요청하고 받을 수 있다.
•
블록을 전송받기 위해서는 먼저 헤더를 받고, 헤더 정보를 기반으로 블록 전체 데이터를 요청한다.
1.
getheaders : 헤더가 필요한 시작 블록과 끝 블록을 포함하여 보낸다.
2.
headers : 블록의 헤더 데이터와 트랜잭션의 수를 보낸다.
•
블록 헤더 검증
◦
작업증명이 올바른가?
◦
이전 블록과 현재 블록의 연결이 정확한가?
◦
매 2016블록마다 직접 계산한 난이도가 정확한가?
#블록 헤더 검증 코드
for _ in range(19):
getheaders = GetHeadersMessage(start_block=previous.hash())
node.send(getheaders)
headers = node.wait_for(HeadersMessage)
for header in headers.blocks:
if not header.check_pow(): # <1>
raise RuntimeError('bad PoW at block {}'.format(count))
if header.prev_block != previous.hash(): # <2>
raise RuntimeError('discontinuous block at {}'.format(count))
if count % 2016 == 0:
time_diff = previous.timestamp - first_epoch_timestamp
expected_bits = calculate_new_bits(previous.bits, time_diff) # <4>
print(expected_bits.hex())
first_epoch_timestamp = header.timestamp # <5>
if header.bits != expected_bits: # <3>
raise RuntimeError('bad bits at block {}'.format(count))
previous = header
count += 1
Python
복사
블록 교환
•
블록 헤더 정보가 있다면 피어에게 블록 데이터를 요청할 수 있다.
1.
getblocks: getheaders와 유사하다. 필요한 블록 높이의 시작과 끝을 요청한다.
2.
blocks: 블록의 헤더정보와 트랜잭션들을 전송한다.