이더리움 Discovery 프로토콜 톺아보기
중앙 서버 없이 노드를 찾는 방법

이더리움 네트워크는 전 세계 곳곳에 분산되어 있는 수많은 노드들이 서로 연결되고, 블록과 트랜잭션을 주고받으며 하나의 네트워크를 형성합니다. 저는 여기에는 한 가지 궁금증이 생겼는데요.
새로운 노드는 처음 실행되었을 때, 다른 이더리움 노드가 어디에 있는지 어떻게 알 수 있을까요?
이 문제를 해결하기 위해 이더리움에는 Discovery라는 네트워크 탐색 메커니즘이 존재합니다. Discovery는 새로운 노드가 기존 네트워크에 참여하기 위해 주변 노드를 찾고, 점진적으로 더 많은 피어 정보를 수집하며, 최종적으로 이더리움 P2P 네트워크에 연결될 수 있도록 도와주는 기술입니다.
이번 글에서는 이더리움 Discovery가 왜 필요한지, 노드는 어떻게 서로를 찾는지, 그리고 Discovery 과정이 어떻게 동작하는지 살펴보겠습니다.
이더리움 Discovery 프로토콜이란?

이더리움 Discovery 프로토콜은 P2P 네트워크에서 연결할 다른 노드를 찾아내기 위한 분산형 주소록 프로토콜입니다. 노드의 IP 주소, 포트, 공개키, 지원 기능 등의 정보를 수집하고, 실제로 통신할 수 있는 피어 후보를 다른 노드와 공유합니다. 헷갈리시면 안되는게 Discovery 프로토콜이 블록이나 트랜잭션을 직접 전달하는 것은 아닙니다.
이후 실행 레이어(Execution Layer)는 RLPx/DevP2P 연결을 생성하고, 합의 레이어(Consensus Layer)는 libp2p 연결을 생성하여 노드 간 메시지를 주고받습니다. 즉, Discovery는 “누구와 연결할지 찾는 단계”이고, DevP2P나 libp2p는 “찾은 노드와 실제로 통신하는 단계”입니다.
서로를 찾기 위한 노드 식별자 — Node ID
Discovery 프로토콜을 본격적으로 알아보기에 앞서, 프로토콜 설계에 적용된 주요 패턴과 개념을 먼저 이해할 필요가 있습니다. 그 첫 번째 개념은 바로 노드 식별자(Node Identifier)입니다.
각 이더리움 노드는 자신을 식별하기 위한 고유한 암호학적 신원을 가지고 있습니다. 모든 노드는 secp256k1 개인키를 생성하며, 이 개인키에서 파생된 공개키가 노드를 식별하는 Node ID가 됩니다.
이더리움 노드는 Discovery 프로토콜을 통해 다른 노드를 찾을 때 IP 주소와 UDP·TCP 포트, 공개키 또는 Node ID 등의 정보가 포함된 노드 주소를 사용합니다. 이때 IP 주소와 포트는 노드에 접근하기 위한 위치를 나타내고, Node ID는 해당 노드의 고유한 신원을 나타냅니다.
따라서 동일한 IP 주소에서 여러 노드가 실행되더라도, 각 노드가 서로 다른 개인키와 Node ID를 가지고 있다면 Discovery 네트워크에서는 서로 다른 노드로 인식됩니다.
Kademlia DHT
다음으로, Discovery 프로토콜 공식 스펙에서 이를 “Ethereum node 정보를 저장하는 Kademlia-like DHT”라고 설명합니다. 쉽게 말하면 전체 노드 목록을 하나의 중앙 서버가 관리하는 것이 아니라, 네트워크에 참여하는 여러 노드가 전체 목록의 일부를 나누어 저장하고 관리하는 구조입니다.
‘DHT’ Distributed Hash Table

Kademlia DHT에서 ‘DHT’는 Distributed Hash Table, 즉 분산 해시 테이블입니다. 중앙 서버 없이 여러 노드가 나누어 관리하는 하나의 거대한 검색 테이블이라고 볼 수 있습니다. 여러 P2P 노드는 Kademlia 프로토콜에 따라 정의된 데이터 포맷으로 키 공간을 나누어 저장합니다.
각 노드는 프로토콜에 따라 자신의 Node ID를 기준으로 XOR 거리(아래에서 설명합니다)를 계산하여 노드 정보를 저장합니다. 만약 특정 노드가 자신보다 멀리 있거나 저장 조건에 맞지 않는 경우에는 해당 정보를 저장하지 않습니다. 하지만 다른 노드가 해당 정보를 저장하고 있기 때문에, 탐색 과정에서 결국 원하는 노드를 찾을 수 있습니다. 이처럼 Discovery 프로토콜은 네트워크에 합류한 노드의 정보를 여러 노드가 나누어 저장하고 관리하는 구조이기 때문에 분산 해시 테이블(DHT)이라고 합니다.
Kademlia
Kademlia는 중앙 서버 없이 P2P 네트워크에서 노드나 데이터를 빠르게 찾기 위한 프로토콜이자 분산 탐색 알고리즘입니다. Kademlia는 각 노드의 Node ID를 keccak256 해시로 계산하여 고정 길이의 ID를 만들고, 각 ID 사이의 XOR 거리(Kademlia Distance)를 기준으로 라우팅합니다. 즉, Kademlia의 라우팅 테이블은 단순히 “가까운 노드 목록”을 저장하는 것이 아니라, Node ID의 거리 구간별로 대표 노드를 저장하는 구조입니다.
이후 이러한 특징을 활용하여 누군가 자신이 가지고 있지 않은 노드 정보를 요청하면, 목표 노드와의 거리를 점점 좁혀가면서 다른 이웃 노드에게 요청을 전달해 원하는 노드를 찾을 수 있도록 도와줍니다. 이 과정은 아래에서 자세히 다루도록 하겠습니다.
Kademlia Distance
먼저 Kademlia에서 말하는 “거리”는 서울과 부산 사이의 거리처럼 물리적인 거리를 의미하거나, 네트워크 지연시간이 짧다는 의미가 아닙니다. discv4 스펙에서는 두 Node Key 사이의 distance를 다음과 같이 정의합니다.
distance(n1, n2) = keccak256(n1) XOR keccak256(n2)
A = 10110100
B = 10110010
C = 00110100예를 들어 A, B, C라는 3개의 노드가 있다고 가정해보겠습니다. 세 노드는 위와 같이 각각 8비트의 Node Key를 가지고 있습니다. 실제로는 keccak256 해시값을 사용하기 때문에 256비트이지만, 이해를 돕기 위해 여기서는 8비트로 단순화하여 설명합니다.
distance(A, B) = 10110100 XOR 10110010 = 6
distance(A, C) = 10110100 XOR 00110100 = 128즉, 노드 A를 기준으로 보면 C보다 B와의 XOR 수치(distance)가 더 작기 때문에, Kademlia의 거리 기준에서는 B가 A에 더 가까운 노드라는 의미입니다.
Routing Table

각 노드(Local Node)는 자신이 알고 있는 다른 노드들의 정보를 routing table에 저장합니다. 이 routing table은 여러 개의 바구니 역할을 하는 bucket으로 구성됩니다.
discv4 기준으로 각 노드는 거리 구간별로 256개의 bucket을 가지며, 각 bucket에는 최대 k = 16개의 노드 엔트리를 저장할 수 있습니다. 따라서 이론상 하나의 routing table에는 최대 4,096개의 Node ID를 저장할 수 있습니다. 각 bucket은 아래와 같은 공식을 기준으로, 해당 거리 구간에 포함되는 노드를 자신의 bucket 안에 저장합니다.
bucket i ⇒ ≤ distance(local, node) <
즉, Local Node와의 거리가 가까운 노드는 앞쪽 bucket index에 저장되고, 거리가 먼 노드일수록 뒤쪽 bucket index에 저장됩니다.
distance(A, B) = 10110100 XOR 10110010 = 6
bucket 0: 1
bucket 1: 2 ~ 3
bucket 2: 4 ~ 7
bucket 3: 8 ~ 15 ✔️
bucket 4: 16 ~ 31
...예를 들어 Node A를 기준으로 Node B와의 거리는 6입니다. 6은 4 ≤ distance < 8 범위에 포함되므로, Node B는 bucket 2에 기록된다고 볼 수 있습니다.
다만 특정 bucket이 이미 가득 찬 상태라면, 가장 오래된 노드에게 PING를 보내 해당 노드가 아직 살아 있는지 확인합니다. 응답이 정상적으로 돌아오면 기존 노드를 그대로 유지하지만, 응답이 없다면 해당 노드를 삭제하고 그 자리에 새로운 노드를 기록합니다.
새 노드는 어떻게 처음 네트워크에 들어가나?
앞서 추상적인 개념에 대해 알아봤고, 이제 Discovery 프로토콜이 실제로 어떻게 동작하는지 첫 단계부터 알아보겠습니다.
모든 노드는 서로 동기화되어 정보(블록, 메시지 등)를 공유하기 전에, 먼저 서로를 탐색하기 위한 노드 식별자 정보를 주고받으며 상대 노드를 판단합니다. 하지만 처음 이더리움 네트워크에 합류하는 새 노드는 어디에 다른 노드가 있는지 아무것도 모르는 상태입니다.
지금부터 완전히 새롭게 네트워크에 합류하려는 Node A와, Node A가 처음 연결을 시도하는 Node B를 예시로 다뤄보겠습니다. Node ID는 원래 32 bytes이지만, 예시에서는 이해를 돕기 위해 8비트로 설명하겠습니다.
bootnode — 초기 진입점
처음 노드를 구축할 때는 bootnode 정보를 입력하여, 해당 노드(bootnode)로부터 다른 노드의 정보를 얻어야 합니다. 이후 전달받은 노드 정보를 바탕으로 Discovery 프로토콜에 따라 다른 노드들을 탐색하고 동기화 작업을 시작합니다. 이러한 과정을 통해 자연스럽게 이더리움 네트워크에 합류하게 됩니다. 따라서 bootnode는 “중앙 서버”라기보다는 네트워크 탐색을 시작하기 위한 초기 진입점입니다.
bootnode를 직접 모르더라도 괜찮습니다. 노드를 구축하는 클라이언트(geth, reth 등)에는 기본적으로 재단(이더리움 재단, 아비트럼 재단 등)에서 운영하는 bootnode가 2~4개 정도 등록되어 있기 때문에, 별도로 bootnode를 입력하지 않더라도 해당 노드들로부터 다른 노드 탐색을 시작할 수 있습니다.
Node A는 아직 알고 있는 친구가 없어서, 현재 bootnode 역할을 하는 Node B만 알고 있다고 가정해보겠습니다.
나와 가까운 노드를 찾는 시간 — lookupSelf()
Discovery 프로토콜은 하나의 Node ID를 기준으로 가까운 노드를 탐색하고, 해당 정보를 routing table에 채웁니다. 이때 탐색의 기준이 되는 Node ID를 target ID라고 하며, 처음 구동된 Node A는 자신의 Node ID를 target ID로 설정해 탐색을 시작합니다. 이 과정을 lookupSelf() 라고 합니다.
Node A는 다른 노드에게 _“이 target ID와 가까운 노드 정보를 알려달라”_는 요청을 보내야 합니다. 이때 어떤 노드에게 요청할지는 loadSeedNodes()가 결정하며, bootnode에 등록된 노드 중 최대 3개(geth)에게 병렬로 요청합니다. 현재 Node A가 알고 있는 bootnode가 Node B 하나뿐이라면, Node B에게만 요청을 보내게 됩니다.
랜덤으로 노드를 찾는 과정 — lookupRandom()
자신의 Node ID를 target ID로 설정하여 lookupSelf()를 한 번 수행했다고 해서 탐색이 끝나는 것은 아닙니다. 이후에는 랜덤한 target ID 3개를 생성하여 동일한 탐색 과정을 한 번 더 수행합니다.
Kademlia routing table은 자신의 주변 노드만 알고 있어서는 안 됩니다. 이후 어떤 target ID에 대한 탐색 요청이 들어오더라도 해당 방향으로 탐색을 이어갈 수 있어야 하기 때문에, Node ID 공간의 여러 영역에 존재하는 대표 노드들을 골고루 알고 있어야 하기 때문입니다.
Wire Protocol
lookupSelf()와 lookupRandom()은 두 노드가 통신을 통해 서로 메시지를 주고받으며 DHT에 필요한 노드 정보를 수집하는 과정입니다. 이때 노드 간 통신과 메시지 형식은 Wire Protocol에 정의된 규칙에 따라 진행됩니다.
PING/PONG
A ── 0x01 PING ──▶ B
A ◀─ 0x02 PONG ─── BWire Protocol에서 가장 먼저 이루어지는 요청은 PING/PONG입니다. 이 과정에서는 UDP로 메시지를 주고받으며 서로를 검증합니다. 즉, 상대 노드가 자신이 전달한 데이터를 실제로 수신하고 응답할 수 있는 노드인지 확인하는 절차를 먼저 수행합니다.
PING
packet = hash || signature || packet-type || RLP(packet-data)PING 패킷의 데이터 구조는 Wire Protocol에 따라 정의되어 있으며, 위와 같은 형태의 메시지를 전송합니다.
0x01 PING
0x02 PONG
0x03 FINDNODE
0x04 NEIGHBORS
0x05 ENRREQUEST
0x06 ENRRESPONSEpacket-type은 요청 종류에 따라 위와 같은 값이 사용됩니다. 현재는 PING 요청이므로 0x01이 들어갑니다.
[
version, # discv4 프로토콜 버전, 일반적으로 4
from, # 송신자가 주장하는 자신의 IP, UDP 포트, TCP 포트
to, # 송신자가 알고 있는 상대방의 IP와 UDP 포트
expiration, # 패킷 만료 시각인 UNIX timestamp
enr-seq # 송신자 ENR의 현재 sequence number
]packet-data는 위와 같이 송신자에 대한 정보가 작성됩니다.
특히, enr-seq는 EIP-868에서 추가된 선택 필드입니다. 노드의 IP, Port 등 식별 정보가 변경되면 sequence 값을 증가시키는데, 따라서 이미 알고 있는 Node ID라고 하더라도 sequence 값이 달라졌다면 최신 정보를 다시 요청해야 합니다. 상대방이 자신이 보유하고 있는 ENR보다 높은 sequence 값을 알려오면, 이후 ENRRequest를 통해 최신 ENR을 요청할 수 있습니다.
from = [sender-ip, sender-udp-port, sender-tcp-port]
to = [recipient-ip, recipient-udp-port, 0]from에는 송신자인 Node A의 IP와 UDP/TCP Port가 들어가고, to에는 현재 요청을 보내는 대상인 Node B의 IP와 UDP Port 정보가 들어갑니다.
hash = keccak256(signature || packet-type || packet-data)
signature = sign(packet-type || packet-data)PING 패킷 전체는 위와 같은 방식으로 서명됩니다. 따라서 패킷 자체에 Node ID가 포함되어 있지 않더라도, 수신자는 서명을 복구(Recovery)하여 송신자의 공개키와 Node ID를 알아낼 수 있습니다.
from # B의 Node ID
IP # 응답자의 IP 주소
ptype # 응답 메시지 타입. PING을 보냈으니 PONG이 와야함
deadline # PONG을 기다릴 마지막 시각(500 ms)
PING.hash # PING의 hash. callback 변수에 저장됨.A는 PING을 전송한 뒤 해당 PING에 대응하는 유효한 PONG을 기다리는 상태를 생성합니다. geth를 예시로 보면, 이를 구현하기 위해 pending reply 상태를 로컬 메모리에 관리합니다. 해당 상태에는 위와 같은 정보가 저장되며, 이후 PONG 패킷을 수신했을 때 예상했던 응답이 맞는지 검증하는 데 사용됩니다.
이 중에서도 PONG.ping-hash가 PING.hash와 일치하는지가 중요하고, geth 기준으로 약 500밀리초 이내에 PONG이 도착해야 합니다.
PONG
Node B는 전달받은 PING을 검증합니다. “이 PING이 최소한 정상적인 요청인지” 확인하고, 응답에 필요한 정보를 준비합니다. 이 과정에서 PING 패킷의 데이터와 서명을 이용해 Node A의 Node ID를 알아낼 수 있으며, 이를 통해 어떤 노드가 해당 PING을 전송했는지 확인할 수 있습니다.
packet = hash || signature || packet-type || RLP(packet-data)서명 복구가 끝나면 B는 PING을 보낸 A에게 PONG 메시지를 전송합니다. 마찬가지로 패킷의 전체 구조는 Wire Protocol에 따라 위와 같은 형태를 사용합니다.
[
to, # PING UDP 패킷에서 관찰한 송신자의 IP와 UDP 포트
ping-hash, # 송신자가 보낸 PING 패킷의 해시
expiration, # PONG 만료 시각
enr-seq # 수신자의 현재 ENR sequence number
]다만 packet-data의 내용은 PING과 다릅니다. 여기서 가장 중요한 필드는 ping-hash입니다. 이를 통해 A는 해당 PONG이 자신이 조금 전에 보낸 PING에 대한 응답인지 확인할 수 있습니다. 요청하지 않은 PONG이거나 ping-hash가 일치하지 않는 경우에는 unsolicited reply로 처리됩니다. 정상적인 PONG이라면 마지막 PONG 수신 시각을 Node DB에 저장합니다.
이 과정을 통해 두 노드는 상대방이 자신의 개인키를 보유한 노드임을 확인하고, 패킷에 포함된 IP와 Port를 기반으로 이후 메시지를 주고받을 준비가 되었음을 확인합니다.
Endpoint Proof — PING/PONG 한번 더
이번에는 반대로 B가 A에게 PING을 보냅니다. 그 이유는 요청을 보낸 A가 패킷에 기록된 출발지 IP와 UDP Port에서 실제로 응답할 수 있는 노드인지 B가 확인하기 위해서입니다. 즉, A의 신뢰성이나 정직함을 증명하는 것이 아니라 A의 UDP 주소가 위조된 것이 아닌지를 확인하는 과정입니다.
M ── FINDNODE ──▶ B ── NEIGHBORS ──▶ V이 과정이 필요한 이유는 UDP의 출발지 IP를 위조할 수 있기 때문입니다. 예를 들어 Node M이 자신의 IP를 Node V의 IP로 위조해 B에게 요청하면, B의 응답은 M이 아닌 V에게 전달됩니다. 이를 반복하면 V가 원치 않는 트래픽을 받는 증폭 공격으로 이어질 수 있습니다.
따라서 Endpoint Proof는 요청을 보낸 노드의 IP와 UDP Port가 실제로 해당 노드의 주소인지 확인하는 과정입니다. B가 A에게 PING을 보내고, A가 해당 주소에서 정상적인 PONG을 반환하면 검증됩니다.
이 검증은 최근 12시간 동안 유효하며, 만료되면 필요에 따라 다시 PING/PONG을 수행합니다.
FINDNODE
A ── 0x03 FINDNODE(Target id) ──▶ BEndpoint Proof를 위한 PING/PONG 과정까지 완료되면, Node A는 B에게 FINDNODE 요청을 보냅니다. 이 요청은 “Node B의 Routing Table에 있는 노드 중에서 Target ID와 XOR 거리가 가까운 노드들을 알려달라”는 의미입니다.
이때 B는 A에 대한 Endpoint Proof를 보유하고 있어야 합니다. 공식 사양에서는 트래픽 증폭 공격을 방지하기 위해, FINDNODE를 보낸 노드가 Endpoint Proof를 통해 검증된 경우에만 NEIGHBORS 응답을 보내도록 정의하고 있습니다.
NEIGHBORS
A ── 0x03 FINDNODE(Target id) ──▶ B
A <── 0x04 NEIGHBORS #1 ── B
A <── 0x04 NEIGHBORS #2 ── BB는 자신의 로컬 Routing Table에서 Target ID와 가장 가까운 노드를 최대 16개 선택하여 NEIGHBORS 메시지로 반환합니다.
nodes = [[ip, udp-port, tcp-port, node-id], ...]패킷에는 각 노드에 접근하기 위해 필요한 최소한의 주소 정보가 목록 형태로 담깁니다. discv4의 UDP 패킷은 최대 1280바이트이기 때문에, 16개 노드의 정보를 하나의 패킷에 모두 담을 수 없는 경우에는 B가 여러 개의 NEIGHBORS 패킷으로 나누어 전송할 수 있습니다.
Recursive Lookup(재귀적 탐색) 동작
A <── 0x04 NEIGHBORS(C,D,E) ── B
A ── PING/PONG/FINDNODE(T) ──▶ C
A ── PING/PONG/FINDNODE(T) ──▶ D
A ── PING/PONG/FINDNODE(T) ──▶ E재귀적 탐색은 특정 Node ID와 XOR 거리가 가장 가까운 노드들을 네트워크 전체에서 점진적으로 찾아가는 탐색 방식입니다. NEIGHBORS를 통해 Target과 가까운 노드 정보를 전달받았다고 해서 탐색이 끝나는 것은 아닙니다. 전달받은 노드들에게 다시 동일한 과정을 반복하며 더 가까운 노드를 찾아갑니다.
이때 최대 3개의 노드에게 병렬로 요청을 보낼 수 있으며, 이 과정을 반복할수록 탐색 범위는 점점 Target 주변으로 좁혀집니다. discv4 표준에 따르면 현재까지 발견한 가장 가까운 16개 노드 중 아직 조회하지 않은 노드들에 대한 질의가 모두 완료되면 탐색을 종료합니다.
마무리
지금까지 Discovery 프로토콜에 대해 알아봤습니다.
bootnode를 시작점으로 다른 노드를 탐색하고, 동기화와 메시지 교환을 위한 Peer를 찾아가는 전체 과정을 살펴봤습니다. 이를 통해 이더리움 노드가 Discovery 프로토콜을 이용해 네트워크 안에서 서로를 찾아가는 구조도 이해할 수 있었습니다.
이러한 이더리움 고유의 네트워크 프로토콜은 노드를 직접 구축하고 운영하는 블록체인 개발자에게 필요한 지식이라고 생각합니다. 실제 현업에서도 노드 연결이나 P2P 문제를 이해하는 데 도움이 되는 내용이기 때문에 이번 기회에 정리해봤습니다.