4️⃣

네트워크

오버레이 네트워크

인터넷 망은 물리적 네트워크과 논리적 네트워크로 구성된다.
논리적 링크에 의해 컴퓨터간의 연결을 오버레이 네트워크라 한다.
예를들어 한국에 있는 서버와 유럽에 있는 서버는 여러 물리적 컴퓨터의 라우팅을 통해 구성되지만, 논리적 연결을 통해 피어를 맺을 수 있다.
오버레이 네트워크는 네트워크 구성을 탄력적으로 할 수 있고, 이를 통해 다수의 사용자와 멀티캐스트가 가능하다는 장점이 있다.
오버레이 네트워크의 이해 (출처: https://www.baeldung.com/cs/underlay-overlay-network)

오버레이 네트워크의 분류

구조화 오버레이
연결 상대가 미리 정해진 상태를 의미한다.
메시지 수신이 보장되며 확장성이 매우 높다.
네트워크 구성의 유연성이 떨어진다.
구조화 오버레이의 예
비구조화 오버레이
인접노드를 알아서 선택한다.
토폴로지가 규정되어 있지 않다.
유연성 있는 전달이 가능하다.
메시지 수신을 보장하지 못한다.
수퍼노드가 지역적으로 서버의 역할을 수행하기도 한다.
비트코인은 비구조화 오버레이를 채택하고 있다.
비트코인의 경우 노드의 수가 충분해짐에 따라 수퍼노드의 필요성이 없어졌다.
대다수의 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: 블록의 헤더정보와 트랜잭션들을 전송한다.