🌍 네트워크

libp2p

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 홀펀칭 성공률도 높아요."
💬 P2P 연결 이슈 디버깅에서
"피어 연결이 안 되는 건 NAT 문제일 가능성이 높아요. libp2p AutoNAT 로그를 보면 NAT 타입이 나오는데, Symmetric NAT면 직접 연결이 어려워서 Circuit Relay를 통해야 합니다. relay 노드를 구성하거나 QUIC 홀펀칭을 활성화해보세요."
💬 IPFS 클러스터 구축 미팅에서
"IPFS 노드 간 통신은 libp2p가 다 처리해줍니다. Bitswap 프로토콜로 블록 교환하고, Kademlia DHT로 콘텐츠 프로바이더를 찾죠. 프라이빗 네트워크 만들려면 libp2p PSK(Pre-Shared Key)를 설정해서 외부 노드 연결을 차단하면 됩니다."

⚠️ 흔한 실수 & 주의사항

DHT 부트스트랩 누락

새 노드가 네트워크에 참여하려면 최소 1개 이상의 부트스트랩 노드가 필요합니다. 부트스트랩 없이 시작하면 고립된 노드가 됩니다. 공용 부트스트랩 노드를 설정하거나 프라이빗 네트워크용 시드 노드를 운영하세요.

GossipSub 메시지 크기 제한 무시

GossipSub 기본 메시지 크기 제한은 1MB입니다. 큰 데이터를 전송하면 메시지가 드롭됩니다. 대용량 데이터는 Bitswap이나 청크 분할을 사용하고, GossipSub은 메타데이터나 알림용으로 사용하세요.

프로토콜 버전 관리 실패

프로토콜 ID에 버전을 명시하지 않으면("/my-app" vs "/my-app/1.0.0") 호환성 문제가 발생합니다. 프로토콜 변경 시 새 버전을 추가하고, 구버전 핸들러도 유지하여 점진적 마이그레이션을 지원하세요.

올바른 libp2p 설정

QUIC 트랜스포트 우선 사용(NAT 친화적), 여러 부트스트랩 노드 설정(3개 이상), 프로토콜 버전 명시(/protocol/x.y.z), DHT 클라이언트 모드는 저사양 기기에서만 사용, PeerStore 영속화로 재시작 시 빠른 연결.

🔗 관련 용어

📚 더 배우기