libp2p
Library for Peer-to-Peer
IPFS에서 시작된 모듈화된 P2P 네트워킹 라이브러리. 프로토콜 협상, 피어 발견, NAT 홀펀칭을 지원하며 Filecoin, Polkadot 등 블록체인에서 사용.
Library for Peer-to-Peer
IPFS에서 시작된 모듈화된 P2P 네트워킹 라이브러리. 프로토콜 협상, 피어 발견, NAT 홀펀칭을 지원하며 Filecoin, Polkadot 등 블록체인에서 사용.
libp2p는 Protocol Labs가 IPFS(InterPlanetary File System)를 개발하면서 탄생한 모듈화된 P2P 네트워킹 라이브러리입니다. 전통적인 클라이언트-서버 모델과 달리, libp2p는 모든 노드가 동등한 피어로 작동하는 탈중앙화 네트워크를 구축합니다. 핵심 철학은 '모듈성'으로, 전송 계층(TCP, QUIC, WebSocket), 보안(TLS, Noise), 멀티플렉싱(yamux, mplex) 등을 자유롭게 조합할 수 있습니다.
프로토콜 협상(Protocol Negotiation)은 libp2p의 핵심 기능입니다. 두 피어가 연결될 때 multistream-select 프로토콜을 사용해 서로 지원하는 프로토콜을 협상합니다. 예를 들어 "/ipfs/kad/1.0.0"(Kademlia DHT)이나 "/meshsub/1.1.0"(GossipSub)처럼 버전이 명시된 프로토콜 ID를 교환하여 호환성을 보장합니다. 이를 통해 네트워크가 진화해도 하위 호환성을 유지할 수 있습니다.
피어 발견(Peer Discovery)은 분산 네트워크에서 다른 노드를 찾는 메커니즘입니다. libp2p는 Kademlia DHT(분산 해시 테이블), mDNS(로컬 네트워크), Bootstrap 노드, Rendezvous 서버 등 다양한 방식을 지원합니다. 특히 DHT는 피어 라우팅뿐 아니라 콘텐츠 라우팅에도 사용되어, 특정 데이터를 가진 피어를 네트워크에서 찾을 수 있습니다.
NAT 트래버설(NAT Traversal)은 방화벽/NAT 뒤에 있는 피어 간 직접 연결을 가능하게 합니다. libp2p는 STUN/TURN과 유사한 AutoNAT(NAT 상태 감지), Circuit Relay(중계 서버를 통한 연결), Hole Punching(UDP/QUIC를 통한 직접 연결) 기술을 제공합니다. 특히 QUIC 기반 hole punching은 성공률이 높아 실제 P2P 통신에 많이 사용됩니다.
실제 프로덕션에서 libp2p는 여러 주요 블록체인 프로젝트의 네트워킹 계층입니다. Filecoin은 libp2p로 스토리지 마이너와 검색 마이너 간 통신을 처리하고, Polkadot/Substrate는 블록 전파와 트랜잭션 가십에 사용합니다. Ethereum 2.0의 비콘 체인도 libp2p 기반이며, 수만 개의 노드가 GossipSub 프로토콜로 블록을 전파합니다.
import { createLibp2p } from 'libp2p'
import { tcp } from '@libp2p/tcp'
import { webSockets } from '@libp2p/websockets'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { kadDHT } from '@libp2p/kad-dht'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { bootstrap } from '@libp2p/bootstrap'
// libp2p 노드 생성
const node = await createLibp2p({
// 전송 계층: TCP + WebSocket 지원
transports: [
tcp(),
webSockets()
],
// 보안 계층: Noise 프로토콜 (TLS 대안)
connectionEncryption: [noise()],
// 스트림 멀티플렉싱: 단일 연결에서 여러 스트림
streamMuxers: [yamux()],
// 피어 발견: Bootstrap 노드 + DHT
peerDiscovery: [
bootstrap({
list: [
'/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa'
]
})
],
// 서비스: DHT + PubSub
services: {
dht: kadDHT({
clientMode: false // 풀 DHT 노드로 참여
}),
pubsub: gossipsub({
emitSelf: false,
gossipIncoming: true,
floodPublish: true
})
}
})
// 노드 시작
await node.start()
console.log('PeerID:', node.peerId.toString())
console.log('Listening on:', node.getMultiaddrs())
// 커스텀 프로토콜 핸들러 등록
await node.handle('/my-app/1.0.0', async ({ stream }) => {
// 스트림에서 데이터 읽기
for await (const chunk of stream.source) {
console.log('Received:', new TextDecoder().decode(chunk.subarray()))
}
})
// PubSub 토픽 구독
node.services.pubsub.subscribe('my-topic')
node.services.pubsub.addEventListener('message', (evt) => {
console.log(`Topic: ${evt.detail.topic}, Data: ${evt.detail.data}`)
})
// 메시지 발행
await node.services.pubsub.publish('my-topic',
new TextEncoder().encode('Hello P2P!')
)
package main
import (
"context"
"fmt"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
dht "github.com/libp2p/go-libp2p-kad-dht"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/p2p/security/noise"
"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
quic "github.com/libp2p/go-libp2p/p2p/transport/quic"
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
)
func main() {
ctx := context.Background()
// libp2p 호스트 생성
host, err := libp2p.New(
// 전송 계층: TCP + QUIC
libp2p.Transport(tcp.NewTCPTransport),
libp2p.Transport(quic.NewTransport),
// 리스닝 주소
libp2p.ListenAddrStrings(
"/ip4/0.0.0.0/tcp/9000",
"/ip4/0.0.0.0/udp/9000/quic-v1",
),
// 보안: Noise 프로토콜
libp2p.Security(noise.ID, noise.New),
// 멀티플렉싱: yamux
libp2p.Muxer("/yamux/1.0.0", yamux.DefaultTransport),
// NAT 홀펀칭 활성화
libp2p.EnableHolePunching(),
libp2p.EnableAutoRelay(),
)
if err != nil {
panic(err)
}
defer host.Close()
fmt.Printf("PeerID: %s\n", host.ID())
fmt.Printf("Addresses: %v\n", host.Addrs())
// Kademlia DHT 초기화
kademliaDHT, err := dht.New(ctx, host,
dht.Mode(dht.ModeServer), // 풀 DHT 노드
dht.BootstrapPeers(dht.GetDefaultBootstrapPeerAddrInfos()...),
)
if err != nil {
panic(err)
}
// DHT 부트스트랩
if err := kademliaDHT.Bootstrap(ctx); err != nil {
panic(err)
}
// GossipSub PubSub 생성
ps, err := pubsub.NewGossipSub(ctx, host,
pubsub.WithPeerExchange(true),
pubsub.WithFloodPublish(true),
)
if err != nil {
panic(err)
}
// 토픽 조인
topic, err := ps.Join("filecoin/blocks")
if err != nil {
panic(err)
}
// 구독
sub, err := topic.Subscribe()
if err != nil {
panic(err)
}
// 메시지 수신 고루틴
go func() {
for {
msg, err := sub.Next(ctx)
if err != nil {
return
}
fmt.Printf("From: %s, Data: %s\n", msg.GetFrom(), msg.Data)
}
}()
// 블록 대기
select {}
}
use libp2p::{
core::upgrade,
gossipsub, identity, kad, noise,
swarm::{NetworkBehaviour, SwarmEvent},
tcp, yamux, PeerId, Swarm, Transport,
};
use std::time::Duration;
use tokio::select;
// 커스텀 네트워크 동작 정의
#[derive(NetworkBehaviour)]
struct MyBehaviour {
kademlia: kad::Behaviour,
gossipsub: gossipsub::Behaviour,
}
#[tokio::main]
async fn main() -> Result<(), Box> {
// 키페어 생성 (PeerID 결정)
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());
println!("PeerID: {}", local_peer_id);
// 전송 계층 구성: TCP + Noise + Yamux
let transport = tcp::tokio::Transport::default()
.upgrade(upgrade::Version::V1)
.authenticate(noise::Config::new(&local_key)?)
.multiplex(yamux::Config::default())
.boxed();
// Kademlia DHT 설정
let store = kad::store::MemoryStore::new(local_peer_id);
let kademlia = kad::Behaviour::new(local_peer_id, store);
// GossipSub 설정
let gossipsub_config = gossipsub::ConfigBuilder::default()
.heartbeat_interval(Duration::from_secs(1))
.validation_mode(gossipsub::ValidationMode::Strict)
.build()
.expect("Valid config");
let gossipsub = gossipsub::Behaviour::new(
gossipsub::MessageAuthenticity::Signed(local_key.clone()),
gossipsub_config,
)?;
// Swarm 생성
let behaviour = MyBehaviour { kademlia, gossipsub };
let mut swarm = Swarm::new(
transport,
behaviour,
local_peer_id,
libp2p::swarm::Config::with_tokio_executor(),
);
// 리스닝 시작
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
// 토픽 구독
let topic = gossipsub::IdentTopic::new("polkadot/transactions");
swarm.behaviour_mut().gossipsub.subscribe(&topic)?;
// 이벤트 루프
loop {
select! {
event = swarm.select_next_some() => match event {
SwarmEvent::NewListenAddr { address, .. } => {
println!("Listening on: {}", address);
}
SwarmEvent::Behaviour(MyBehaviourEvent::Gossipsub(
gossipsub::Event::Message { message, .. }
)) => {
println!(
"Received: {} from {:?}",
String::from_utf8_lossy(&message.data),
message.source
);
}
SwarmEvent::Behaviour(MyBehaviourEvent::Kademlia(
kad::Event::RoutingUpdated { peer, .. }
)) => {
println!("DHT: Added peer {}", peer);
}
_ => {}
}
}
}
}
"libp2p 기반으로 네트워킹 계층을 구축하면 좋겠습니다. Polkadot이나 Filecoin처럼 검증된 프로젝트에서 쓰고 있고, DHT로 피어 발견하고 GossipSub으로 블록 전파하면 됩니다. QUIC 트랜스포트 쓰면 NAT 홀펀칭 성공률도 높아요."
"피어 연결이 안 되는 건 NAT 문제일 가능성이 높아요. libp2p AutoNAT 로그를 보면 NAT 타입이 나오는데, Symmetric NAT면 직접 연결이 어려워서 Circuit Relay를 통해야 합니다. relay 노드를 구성하거나 QUIC 홀펀칭을 활성화해보세요."
"IPFS 노드 간 통신은 libp2p가 다 처리해줍니다. Bitswap 프로토콜로 블록 교환하고, Kademlia DHT로 콘텐츠 프로바이더를 찾죠. 프라이빗 네트워크 만들려면 libp2p PSK(Pre-Shared Key)를 설정해서 외부 노드 연결을 차단하면 됩니다."
새 노드가 네트워크에 참여하려면 최소 1개 이상의 부트스트랩 노드가 필요합니다. 부트스트랩 없이 시작하면 고립된 노드가 됩니다. 공용 부트스트랩 노드를 설정하거나 프라이빗 네트워크용 시드 노드를 운영하세요.
GossipSub 기본 메시지 크기 제한은 1MB입니다. 큰 데이터를 전송하면 메시지가 드롭됩니다. 대용량 데이터는 Bitswap이나 청크 분할을 사용하고, GossipSub은 메타데이터나 알림용으로 사용하세요.
프로토콜 ID에 버전을 명시하지 않으면("/my-app" vs "/my-app/1.0.0") 호환성 문제가 발생합니다. 프로토콜 변경 시 새 버전을 추가하고, 구버전 핸들러도 유지하여 점진적 마이그레이션을 지원하세요.
QUIC 트랜스포트 우선 사용(NAT 친화적), 여러 부트스트랩 노드 설정(3개 이상), 프로토콜 버전 명시(/protocol/x.y.z), DHT 클라이언트 모드는 저사양 기기에서만 사용, PeerStore 영속화로 재시작 시 빠른 연결.