IPFS 는 무엇인가?

IPFS (nterPlanetary File System) 는 분산 Web 을 의미하는 프레임워크 입니다. (Network 계층부터 Application 계층까지)

이는 콘텐츠 기반 주소 (Contents based Addressing) 를 활용한 Peer-to-Peer 하이퍼 미디어 배포 프로토콜 입니다. IPFS 네트워크의 노드는 분산 파일 시스템을 형성합니다. 오픈소스 커뮤니티를 통해 2014년 초 Protocol Labs 에서 개발 한 오픈 소스 프로젝트입니다. (설계: Juan Benet) IPFS 는 파일 시스템의 글로벌 Peer-to-Peer 분산 버전입니다. IPFS 에서 데이터 저장은 조각 형태로 저장되며 각 조각의 크기는 256kb 입니다. (마지막 조각이 256kb 보다 작거나 파일의 전체 크기가 256kb 보다 작으면 조각으로 직접 처리되어 더 이상 새 블록을 차지하지 않음) 네트워크 노드는 조각을 저장합니다. 데이터를 검색 할 때 모든 조각이 수집되고 파일 관리 시스템을 통해 조각을 순차적으로 결합하여 파일을 얻습니다.

전체 네트워크에 중앙 서버가 없는 완전히 분산되어있는 것을 볼 수 있습니다.

공식 웹 사이트: https://ipfs.io/


IPFS 디자인 - Network

IPFS는 다양한 기능을 담당하는 하위 프로토콜을 조합하여 구성된다.

  • Identities
  • Network
  • Routing
  • Exchange
  • Discovery
  • Objects
  • Files
  • Naming

IPFS 를 구성하는 많은 모듈들은 별도의 프로젝트로 진행되고 있다.

차분히 하나씩 알아보는 시간을 가지고자 한다.

1. Network

IPFS의 Network 기능은 독립 프로젝트인 libp2p 를 사용하고 있다.

1.1 libp2p

P2P Network 를 구축하는데 발생하는 다양한 문제를 해결하는 것을 목적으로 한 라이브러리 모음이다. github.com/libp2p/specs

IPFS 는 장착된 모듈을 활용하여 내부에서 이용하는 P2P Network 를 구축한다.

공식 구현은 다음과 같다.

1.2 Transport agnostic

3.1 Transport agnostic - github.com/libp2p/specs

P2P 시스템은 Internet 위에 Overlay Network 를 구축한다.

libp2p 는 TCP(UDP)/IP에는 의존하지 않고 Ethernet, Bluetooth 등 다양한 Transport Protocol 에도 마찬가지로 P2P 시스템을 구축하는 것을 목표로 하고있다.

1.2.1 multiaddr (self-describing addressing)

자기 기술형 애드레싱 (Adressing) 이라고 할 수 있다. Address 를 표현하는 문자열에 자신이 이용하고 있는 Protocol 을 포함하는 형식으로 multihash 와 같은 방식을 사용한다.

libp2p 에서는 multiaddr 방식이라고 설명하고 있다.

multiformats/multiaddr

/<protoName string>/<value string> + <protoName string>/<value string> + ...

IPFS 의 Node Address 의 경우 다음과 같이 <multiaddr>+/ipfs/<Node ID> 포맷으로 나타낸다.

/ip4/127.0.0.1/tcp/9000/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu
# Address :
#   IP          : 127.0.0.1
#   tcp         : 9000
#   IPFS NodeID : QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

multihash 의 특징으로서 ~ over ~ 를 다층적으로 표현할 수 있다.

# IPFS over TCP over IPv6 (typical TCP)
/ip6/fe80::8823:6dff:fee7:f172/tcp/4001/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

# IPFS over uTP over UDP over IPv4 (UDP-shimmed transport)
/ip4/162.246.145.218/udp/4001/utp/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

# IPFS over IPv6 (unreliable)
/ip6/fe80::8823:6dff:fee7:f172/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

# IPFS over TCP over DNS4
/dns4/example.com/tcp/4001/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

# IPFS over TCP over IPv4 over TCP over IPv4 (proxy)
/ip4/162.246.145.218/tcp/7650/ip4/192.168.0.1/tcp/4001/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

# IPFS over Ethernet (no IP)
/ether/ac:fd:ec:0b:7c:fe/ipfs/QmYJyUMAcXEw1b5bFfbBbzYu5wyyjLMRHXGUkCXpag74Fu

multiaddr 을 활용한 Overlay Networking

Layer 마다 Protocol 또는 프록시 구성이 명시적으로 표현되어 있는 것을 알 수 있다.

모든 네트워킹 프로토콜이 libp2p 에 의해 구현된 것은 아니다.

multiaddr 자체는 문자열이지만 사용하기 쉽게 하기 위해서는 Parser/Validator 가 필요하다.

Node.js 의 경우 js-multiaddr 를 이용해 Parse 한다. Validation는 js-mafmt 가 담당한다.

Go의 경우 go-multiaddr 를 이용해 Parse 한다. 그 외 multiaddr Instance 를 조작하여 캡슐화/해제 및 터널링을 표현하는 새로운 Instance 를 생성하는 등 편리기능도 가진다.

1.3 Multi-multiplexing

3.2 Multi-multiplexing - github.com/libp2p/specs

libp2p 는 Multi-multiplexing (다중-멀티플랙싱) 개념이 있다. IPFS 에서 이야기 하는 다중-멀티플렉싱의 개념을 설명하는 자료는 링크외에 찾지 못하여 정확한 분석이 어렵다. 대략적인 느낌은 “다양한 유즈케이스에서 사용하는 여러 네트워킹 중 가능한 작은 수 (적합한 네트워크) 를 선택할 수 있도록 유연한 설정을 제공” 한다는 것으로 아래와 같다:

  • 여러 listen 네트워크 인터페이스를 다중화
  • 여러 전송 (Transport) 프로토콜을 다중화
  • Peer 당 여러 연결 (Connection) 을 다중화
  • 여러 클라이언트 프로토콜을 다중화
  • 연결 당, 프로토콜 당 여러 스트림을 다중화 (SPDY, HTTP2, QUIC, SSH)
  • flow control (backpressure, fairness)
  • 서로 다른 임시 키로 각 연결을 암호화

예를 들어, 다음과 같은 단일 IPFS 노드를 상상:

  • 특정 TCP/IP 주소에서 listen
  • 서로 다른 TCP/IP 주소에서 listen
  • SCTP/UDP/IP 주소를 listen
  • UDT/UDP/IP 주소를 listen
  • 다른 노드 X 에 대한 여러 (multiple) 연결
  • 다른 노드 Y 에 대한 여러 연결
  • 연결 당 여러 스트림이 open
  • HTTP2 를 통해 스트림을 노드 X 에 다중화 (multiplexes)
  • SSH 를 통해 스트림을 노드 Y 에 다중화
  • libp2p의 맨 위에 마운트 된 하나의 프로토콜은 하나의 피어 당 하나의 스트림을 사용
  • libp2p의 맨 위에 마운트 된 하나의 프로토콜은 피어 당 여러 (multiple) 스트림 사용

이 정도의 유연성을 제공하지 않으면 다양한 플랫폼, 유즈케이스 또는 네트워크 설정에서 libp2p 를 사용할 수 없다. 필요로하는 것을 정확하게 사용할 수 있도록 충분히 유연하다. 사용자 또는 응용 프로그램 제약 조건의 복잡함이 옵션으로 libp2p 를 배제하지 않도록 할 수 있다.

1.3.1 Transports

Peer 간 접속을 확립할 때 Transports 라는 인터페이스 사양으로 정의되어 있다.
TCP, UDP, WebRTC, WebSocket 등의 다양한 Transport Protocol 을 사용 가능하게 하는 것을 목적으로 하고 있다.

libp2p/interface-transport

libp2p Transports

  • Client
    • Dial (Client ) 기능
    • multiaddr 에 의한 접속처 Peer 지정이 가능
    • 돌려받는 Connection 은 interface-connection 을 제공하여야 함
  • Server
    • Listen (Server) 기능
    • Handler 에 Connection 은, interface-connection 을 제공하여야 함
  • 인스턴스화
    • 예를 들어 WebRTC 의 Signaling 정보는 Dial/Listen 에서 공통적으로 이용하므로 인스턴스화하여 상태를 보관

1.3.2 Connection

Transporter 에 의해 작성된 Connection 의 action 은 Connection 인터페이스 사양으로 정의되어 있다.

libp2p/interface-connection

  • Read/Write
  • Full/Half Duplex 서포트
  • 플로우 제어
    • Back-Pressure
  • 절단 처리
    • Half-Close

Transports 와 Connection 을 구성한 라이브러리는 아래에 정리되어 있다.

Transports - libp2p.io/implementations

  • libp2p-tcp
  • libp2p-quic (Stream Muxer 포함)
  • libp2p-websockets
  • libp2p-webrtc-star
  • libp2p-webrtc-direct
  • libp2p-udp
  • libp2p-utp

Go

Go 의 libp2p Interface 는 go-libp2p-net 프로젝트에서 정의하고 있다.

Transport 에 관한 interface 는 go-libp2p-transport 모듈에 정의하고 있다. (예를 들어 접속을 나타내는 Connlibp2p-net/Conn 을 확장)
즉, 각 Transport 구현은 go-libp2p-transport 의 Interface 를 구현하는 형태가 된다.

다른 Node 에 접속요구 또는 다른 Node 로부터 접속 요구를 수신해 확립된 접속 (ex. net.Conn, websocket.Conn) 은 ① go-multiaddr-net 의해 multiaddr friendly 한 API로 랩핑 된 후, ② go-libp2p-transport-upgrader 의해 Stream 다중화 가능 통신의 암호화 된 Connection으로 Upgrade (후술)된다.

libp2p Transports Interfaces

1.3.2.1 Connection Manager

네트워크 자원을 관리하기 위해서는 전체 Connection 을 종합적으로 감시하는 역할이 필요하다.
이를 위한 역할이 Connection Manager 이다.

Connection Manager

  • 접속 Node 수 제한
  • 대역 제한
    • 수신/송신별 제한
  • Latency 감시

구현

1.3.2.2 Filter

접속하고 싶지 않은 Node 가 있는 경우 Filter 를 설정할 수 있다.
Filter 대상은 multiaddr 에 의해 지정된다.

// make a new filterset
f := NewFilters()

// filter out addresses on the 192.168 subnet
_, ipnet, _ := net.ParseCIDR("192.168.0.0/16")
f.AddDialFilter(ipnet)

// check if an address is blocked
lanaddr, _ := ma.NewMultiaddr("/ip4/192.168.0.17/tcp/4050")
f.AddrBlocked(lanaddr) // = false  

구현

  • Node
    • Transport Class 는 공통으로 filter 메소드를 제공
  • Go

1.3.3 Network Layer

libp2p 에서 Network Protocol 의 구성은 역할별로 몇가지 Layer 로 나뉘어 있다.

TCP/IP 계층 모델의 경우 (OSI 모델에 대략 대응하는 것),

  • Link (물리층, 데이터링크)
  • Internet (네트워크)
  • Transport
  • Application (세션, 프레젠테이션, 어플리케이션)

라고 하는 Layer 를 형성하고 있다. 송신 데이터그램은 Upper 에서 시작되어 Lower 를 향해서 Encapsulation 되며 수신은 Lower 에서 시작되어 Upper 를 향해서 Decapsulation 가 된다.

Encapsulation 과 Decapsulation

libp2p 에서는 대략 다음과 같은 Layer 가 형성되고 있다.

  • Transport Layer
  • Encryption Layer
  • Stream Multiplex Layer
  • Application Layer

Transport/Encryption/Stream Multiplex/Appliation Layer 동작

  • Transport Layer
    • 다른 Peer 와 Connection 구성
    • Protocols
      • TCP
      • UDP, uTP
      • WebSocket
      • WebRTC
  • Encryption Layer
    • Connection 상의 데이터그램을 암호화 및 서명 추가
    • Protocols
      • secio
  • Stream Multiplex Layer
    • Connection 상에 다수의 Stream 을 가상으로 확립
    • Protocols
      • SPDY, HTTP/2
      • yamux
      • mplex
  • Application Layer
    • Application이 이용
    • Protocols
      • DHT
      • BitSwap

libp2p 에서는 Encapsulation 하여 Layer 를 올리는 것을 Upgrade 라고 한다.

Go 구성

Go의 경우 쌍방향 통신을 실시하는 경로 (Connection, Stream) 는 모두 ReadWrite Closer 에 추상되어 각 Layer 의 랩핑으로 실현한다.

libp2p Layer 구성

libp2p Layer 세부 동작

// transport connection
type Conn io.ReadWriteCloser
func (c *Conn) Write(data []byte) error {
  sendMesage(data)
}

// encrypted encapsulation
type encryptedConn {
  conn Conn
}
func (e *encryptedConn) Write(data []byte) error {
  header := createheader(data)
  encrypted := encrypt(data)
  buf := concat(header, encrypted)
  e.conn.Write(buf)
}
func wrapEncryptConn(insecure io.ReadWriteCloser) (*encryptedConn, error) {
  return &encryptedConn{conn: insecure}
}

// stream multiplex encapsulation
type muxedConn {
  conn encryptedConn
}
func (m *muxedConn) Write(data []byte) error {
  header := createheader(data)
  buf := concat(header, data)
  m.conn.Write(buf)
}
func wrapMuxedConn(simple io.ReadWriteCloser) (*muxedConn, error) {
  return &muxedConn{conn: simple}
}

conn, err := transportConn(multiaddr)
econn, err := wrapEncryptConn(conn)
mconn, err := wrapMuxedConn(econn)

mcon.Write(bufferarray)

1.3.4 Encryption

통신의 기밀성, 무결성을 담보하기 위해서는 통신 내용의 암호화 및 변조 감지가 필요하다.

Transport Protocol 에 의해 암호화되어있는 것도 있지만 Transport Protocol 에 의존하지 않고 암호화 및 변조 검출을 위해 이 레이어에서 다시 암호화 및 변조 감지 제공을 한다.

Encryption Layer

PeerID 생성시에 공개 키를 생성하고 있기 때문에 TLS Like 암호화를 하는 것은 어렵지 않다.

TLS 암호화

구현

Crypto channels 라이브러리는 아래에 정리되어 있다.

Crypto channels - libp2p.io/implementations

  • libp2p-secio

Go 의 경우 관련 interface가 go-conn-security 라는 모듈에 정의되어 있다.
현재 secio 만 구현되어 있어 libp2p 의 디폴트라 할 수 있다.

Go 의 경우 Upgrader 에 의해 Transport 의 Connection 을 Upgrade 하는 형태로 적용된다.
Upgrade 된 Connection 을 사용하여 데이터 송신/수신하는 경우 모든 데이터 그램이 이 Layer 의 Encapsulation/Decapsulation 에 의해 수송된다.

Encryption

1.3.4.1 secio

데이터의 암호화와 서명을 실시하는 Protocol 로서 TLS 와 유사함을 알 수 있다.

[handshake]

데이터 교환은 모두 Protocol Buffer 에 의해 이루어지는 것으로 가정한다.

  • [0] 전제 조건
    • Node 마다 PublicKey/Private Key 는 이미 생성되어 있다

전제조건

  • [1] 기본 정보의 교환 (Hello)
    • 1.1. Nonce 를 작성함
    • 1.2. Local Nonce, Local Node PublicKey 사용 (가능한 KeyExchange, Cipher, Hash 방식의 후보를 교환)
      • 후보는 문자열을 , 으로 나눈것으로 우선도가 높은것부터 늘어놓는다. ex P-256, P-384, P-521
      • 디폴트로 이용 가능한 KeyExchange 방식
        • P-256
        • P-384
        • P-521
      • 디폴트로 이용 가능한 Cipher 방식
        • AES-256
        • AES-128
        • Blowfish
      • 디폴트로 이용 가능한 Hash (MAC) 방식
        • SHA256
        • SHA512

기본 정보 교환

  • [2] Node ID검증
    • 1.1 1.2 에서 받은 Remote Node PublicKey가 Node ID와 일치하는지 검증

NodeID 검증

  • [3] 알고리즘 결정
    • 3.1. Local Node, Remote Node 사이에서 결정에 차이가 나오지 않도록, 우선 순위를 정한다
      • SHA256(PublicKey+Nonce)의 큰 쪽이 우선

우선순위 지정

  • 3.2. 서로 대응하고 있는 방식을 비교하고 KeyExchange/Cipher/Hash 방식을 결정
    • 3.1에서 요구한 우선도가 높은 것이 우선된다

교환 방식 비교 결정

  • [4] 키 교환
    • 4.1. KeyExchange 용 PublicKey/PrivateKey 생성
      • 3.2에서 정한 KeyExchange 방식

KeyExchange

  • 4.2. 1.2의 Local Hello, Remote HelloLocal Exchange PublicKey 를 결합하고 Local Node PrivateKey 에 Sign
    • Sign(LocalHello + RemoteHello + LocalExchangePublicKey)
  • 4.3. Exchange PublicKey 과 4.2의 Signature 를 서로 교환

Sign 교환

  • 4.4. 4.3에서 받은Remote Signature 은 1.2에서 받은Remote Node PublicKey 에서 검증

Remote Node PublicKey 검증

  • 4.5. 1.2에서 받은Remote Exchange PublicKeyLocal Exchange PrivateKey 에서 공유된 Secret 를 생성

Sectet 생성

  • [5] 암호키 생성
    • 5.1. Secret bytes를 반으로 나눈다.
      • 각각 Reader 와 Writer 키의 원데이터가 된다
      • 3.1에서 요구한 우선 순위에 의한 2개를 교체
    • 5.2. 5.1에서 얻은 절반의 bytes를 각각 Cipher IV,Cipher Key,HMAC Key의 3가지 bytes로 나뉜다
    • 5.3. 한쪽에서 Cipher IV, Cipher Key, HMAC KeyEtM (Encrypt-then-MAC ) Writer를 구축하고 다른 한쪽에서 EtM Reader를 구축한다
    • 5.4. 5.3의 Writer, Reader 와 결합하여 암호화된 Connection이 완성

암호 생성

포인트는

  • 2의 검증에서 접속하고 싶은 multiaddr 와 PublicKey가 일치함을 알 수 있으므로 제삼자 증명은 불필요
    • 즉, 클라이언트가 multiaddr 로 접속처를 지정한다. ‘이 공개키를 가진 사람과 잇고 싶다’ 라는의미
  • 4.4 로 서명 능력을 체크
    • 이것으로 multiaddr 에서 제시된 공개키/비밀키를 가진 Node 라는 검증이 완료
  • 5에서 Read/Write 의 키를 떠난 이유는 불명
    • tls 실제로도 이렇게 되어있는지는 미확인
  • 5.1에서 2가지 bytes 를 우선 순위로 바꾸는 것은 Local Writer 에 Remote Reader, Local Reader 에 Remote Writer 가 대응하게 하기 위함
[send, receive]

송신되는 데이터그램은 handshake 에서 생성된 Cipher IV, Cipher Key 로 암호화되어 MAC 을 부여해 송신된다.
수신한 데이터그램은 MAC 을 검증하고 Cipher Key, Cipher Key 로 복호화한다.

송수신 데이터그램

1.3.5 Stream Muxer

Stream Multiplex Layer

Transport Protocol 에 의존하지 않고 암호화 된 양방향 통신 가능한 Connection 이 확립되어 있지만 libp2p 는 일반적으로 그들을 직접 사용하는 것이 아니라 Stream 이라는 가상 (논리적) 양방향 통신 채널을 만들고 P2P Protocol 프레임은 Stream 에서 교환한다.

Stream 은 하나의 Connection 에 다중화 (multiplexing) 연결을 만드는 것으로 연결 처리 및 암호화 Protocol Handshake 등의 오버 헤드를 억제한다.

Stream (multiplexing connection)

[multiplexing] 이란

어느 하나의 통신 채널을 사용해 복수의 스트림을 송수신 하는 것이다.

예를 들면 SSH 는 하나의 Connection 으로 복수 Session 을 다루는 기능을 가지고 있다. tmux (terminal multiplexer) 는 하나의 Screen에서 복수의 가상 Terminal을 조작한다. 또 sslh 는 443 Port 하나로 HTTP, SSH, OpenVPN 등 복수의 Protocol 을 Listen 및 Handle 할 수 있다.

Multiplexing-Wikipedia

Multiplexing - Wikipedia

Introduce to HTTP/2 - Google Developers

Introduction to HTTP/2 - Google Developers

Connection 상에서 Stream 을 다중화하기 위한 Stream Muxer 인터페이스 사양이 정의되어 있다.

libp2p/interface-stream-muxer

  • Connection 에 Attach
  • Outgoing
    • 새로운 Stream Open
  • Incomming
    • Stream마다 Listener 등록

구현

각 언어의 구현 된 모듈은 다음과 같다.

Stream muxers - libp2p.io/implementations

  • libp2p-spdy
  • libp2p-multiplex
    • mplex 구현
  • libp2p-yamux

Go 의 경우 관련 interface 가 go-stream-muxer 모듈에 정의되어있다. SPDY, yamux 도 Multiplexing 대응 프로토콜 또는 구현을 통해 각 Protocol 구현 패키지 Factory 에 Connection 을 전달하는 방식으로 널리 이용되고 있다.

Encryption 과 마찬가지로 Upgrader 의해 Connection 을 Upgrade 하는 형태로 적용된다. Upgrade 된 Connection 을 사용하여 데이터 송/수신하는 경우, Stream 식별자 포함 Frame Header 에서 Encapsulation/Decapsulation 되어 전송된다.

Stream interface

Stream Connection

[js-libp2p-spdy]

const spdy = require('spdy-transport') // Nodejs의 SPDY 구현 모듈
const toStream = require('pull-stream-to-stream')
...

const conn = toStream(rawConn) // Pull Stream 을 보통 Stream 으로 변환

const spdyMuxer = spdy.connection.create(conn, { // Stream 을 받고 SPDY Connection 작성
  protocol: 'spdy',
  isServer: isListener
})
[go-smux-yamux]

import (
...
  yamux "github.com/whyrusleeping/yamux" // Go 의 yamux 구현 모듈
)
...

func (t *Transport) NewConn(nc net.Conn, isServer bool) (smux.Conn, error) {
  var s *yamux.Session
  var err error
  if isServer {
    s, err = yamux.Server(nc, t.Config()) // connection 을 받고 yamux 서버 만들기
  } else {
    s, err = yamux.Client(nc, t.Config()) // connection 을 받고 jamux 클라이언트 만들기
  }
  return (*conn)(s), err
}

1.3.6 Protocol-Multiplexing

Protocol-Multiplexing

libp2p 는 다양한 플랫폼 상에서 동일하게 동작함을 목적으로 하고 있기 때문에 여러 Protocol 을 대응하고 있다.
각 Peer 가 복수의 Protocol 에 대응하기 위해 사전 합의 없이 어떠한 방법으로 Protocol 을 선택할 수 있는지 알아보자면 다음과 같다.

libp2p 는 multistream + multistream-select 방식을 취한다.

※ 여기서 말하는 Stream 은 양방향 통신 경로 모두를 의미한다. (Go 에서 말하는 io.ReadWriteCloser)

1.3.6.1 multistream (self-describing protocol/encoding stream)

자가 기술형 프로토콜 스트림이라 할 수 있다.
libp2p 에서는 multistream 방식으로 표현한다. multistream 에서는 Protocol 을 Protocol Name + Version 으로 나타낸다.

multistream - properties - libp2p/specs

/<protoName string>/<version string>

※ /ipfs/<_node id_="">/<_protoName string_="">/<_version string_=""> 과 같은 형식으로 설명되는 부분이 많이 보여 지지만, multistream 사양 자체는 Node의 주소와 연결되는 것이 아니며 구현에도 /ipfs/<_node id_=""> 는 붙어 있지 않다.

multistream 자체는 이를 규정하고 있을 뿐이며 어떤 순서로 Handshake 하고, Buffer 를 보내고, 합의에 이르는지 순서는 multistream-select 가 담당하고 있다.

1.3.6.2 multistream-select

multistream 형식의 데이터 형식과 합의 과정을 규정하고있는 것이 multistream-select 이다.

multiformats/multistream-select

length-prefixed-message

그 전에 length-prefixed-message 에 대해 알아보자.

  • multistream-select 의 모든 Message Buffer 는 length-prefixed-message 라 불리는 형식으로 보내진다
    • 송신하는 Buffer 의 선두에 Buffer 사이즈를 Uvarint 로 부여한다
  • multistream-select 의 모든 Message Buffer 는 마지막에 \n 을 부여한다
    • length-prefixed-message 사이즈는 이것을 포함한 사이즈가 된다

length-prefixed-message size

handshake

우선 multistream 버전을 확인 준다. Dial 측에서 자신의 Protocol, Version 을 보내고 상대방 Protocol, Version 을 반환한다. 그 과정에서 Dial 측, Listen 측 어느쪽이든 검증한다. (실제 Listen 측 검증만으로 끝남)

handshake 검증

ls

Listener 측은 자신이 지원하는 Protocol 이름 목록을 반환한다. 지원하는 Protocol 은 Peer 생성시에 사전에 등록한다.

ls

select

Dialer 측은 자신이 접속하고자 하는 Protocol 이름을 보내고 Listener 측이 OK 이면 Protocol 이름을 Ack 로 돌려 준다.

select

Version 선택은 현재 Node 에만 구현되었지만 SemVer 를 사용한 Version 범위 지정도 가능하다. Semantic Versioning 2.0.0

구현

1.3.7 Swarm

Transport, Connection, Stream 을 내포하고 Dial, Listen 처리 전체 조정을 맡고있는 것이 Swarm 이다. 너무 정보가 없는 Role 이지만 매우 중요한 역할을 담당하고 있다.

Go는 go-libp2p-swarm 이라고 하는 자체 구현이 존재한다. Node.js 는 비슷한 js-libp2p-switch 가 있다.

1.3.7.1 요약

Swarm Overview 는 다음 그림과 같다.

Swarm Overview

  • 복수의 Transport 관리
  • Connection 은 Peer 마다 여러개 유지
  • Filter 의 접속 제한
  • PeerStore 에서 PeerInfo 관리
  • Connection, Stream 의 Listener 에 Handler 를 Attach 할 수 있다
  • Dial Synchronization System 이 되는 기능을 갖는다

1.3.7.2 Multi-Transport

Swarm Multi-Transport

Swarm 은 복수의 Transport 를 관리할 수 있다.
이로 인하여 다른 Peer 와 접속할 때는 상대가 이용 가능한 Transport Protocol 을 선택해 접속해 준다.

예를 들어 /ip4/162.246.145.218/udp/4001/utp/ipfs/QmYJyUM… 라는 multiaddr 에서 Listen 하고있는 Peer 에 연결하는 경우에는 uTP Protocol 을 구현 한 Transport 가 필요하다. 필요한 Transport 가 없는 경우 에러를 발생한다.

1.3.7.3 Multi-Connection

Swarm Multi-Connection

Swarm 내의 데이터 구조는 Connection 은 여러개 가질 수 있게 되어있다. 그러나 구현을 확인하면 복수로 이중화 한다는 의미가 보이지 않는다.

Swarm 은 Connection 을 은닉하고 있어 직접적인 조작은 공개하지 않고 접속 처리는 Stream 을 만들 때의 아래 체크 부분에서 필요하게 되어 이루어진다.

[libp2p/go-libp2p-swarm/swarm.go]

func (s *Swarm) NewStream(ctx context.Context, p peer.ID) (inet.Stream, error) {
  log.Debugf("[%s] opening stream to peer [%s]", s.local, p)

  // Algorithm:
  // 1. Find the best connection, otherwise, dial.
  // 2. Try opening a stream.
  // 3. If the underlying connection is, in fact, closed, close the outer
  //    connection and try again. We do this in case we have a closed
  //    connection but don't notice it until we actually try to open a
  //    stream.
  //
  // Note: We only dial once.
  //
  // TODO: Try all connections even if we get an error opening a stream on
  // a non-closed connection.
  dials := 0
  for {
    c := s.bestConnToPeer(p) // 살아있는 Connection을 찾는다
    if c == nil {
      if dials >= DialAttempts { // const DialAttempts = 1 이므로 시도 1 번
        return nil, errors.New("max dial attempts exceeded")
      }
      dials++

      var err error
      c, err = s.dialPeer(ctx, p) // 여기에서 Dial
      if err != nil {
        return nil, err
      }
    }
    s, err := c.NewStream() // Connection 이 발견되면 Stream 만들기
    if err != nil {
      if c.conn.IsClosed() {
        continue
      }
      return nil, err
    }
    return s, nil
  }
}
...

func (s *Swarm) bestConnToPeer(p peer.ID) *Conn {
  // Selects the best connection we have to the peer.
  // TODO: Prefer some transports over others. Currently, we just select
  // the newest non-closed connection with the most streams.
  s.conns.RLock()
  defer s.conns.RUnlock()

  var best *Conn
  bestLen := 0
  for _, c := range s.conns.m[p] {
    if c.conn.IsClosed() {
      // We *will* garbage collect this soon anyways.
      continue // 닫고있는 녀석은 가비지컬랙션 되므로 방치
    }
    c.streams.Lock()
    cLen := len(c.streams.m)
    c.streams.Unlock()

    if cLen >= bestLen { // Stream 이 많다 = 살아있는 가능성 높다 는 판단
      best = c
      bestLen = cLen
    }
  }
  return best
}

이러한 내용을 보면 살아있는 Connection 하나를 최대한 재사용 하고자 하는 의도처럼 보인다.

1.3.7.4 Dial Synchronization System

Swarm Dial Sync System

Swarm 은 Outbound 트래픽이 초과되지 않도록 Dial 에 대한 연구가 이루어지고 있다.

[DialSync]

Peer 에 대한 Dial 요구가 동시에 여러 온 경우 이전 Dial 처리가 끝날 때까지 기다린다. (단위는 Peer 마다)

Swarm Dial Sync

구현은 여기 를 확인한다.

[Backoff]

이미 연결할 수 없게 된 Peer 대해 계속 접속 요청을 해도 소용없기 때문에 일단 연결에 실패하면 Backoff 목록에 추가되어 다음 식에 나타난 시간이 경과하지 않으면 재시도 할 수 없게된다.

// BackoffBase + BakoffCoef * PriorBackoffs^2
until := (5 + 1 * (retryCount + retryCount)) * time.Second

연결에 성공하면 Backoff 목록에서 제외된다.

구현은 여기 를 확인한다.

1.3.7.5 Multi-Encryption Protocol handled with multistream

Swarm 역할로서 Encryption Protocol 의 Instance 는 외부에서 Injection 되므로 범위 밖이지만 libp2p 의 이념에서 Encryption 은 Peer 끼리 사용할 수 있는 것을 선택할 수 있어야 한다. (multistream을 이용하는 것이 전제된 것으로 예상)

실제로, go-libp2p 는 Encryption에 go-conn-security-multistream 을 이용하고있다.

go-conn-security-multistream 은,

  • Encryption Protocol 을 미리 복수 등록
    • 이것이 multistream의 대응 Protocol
  • Upgrader 에 의해 Connection 이 Upgrade 될 때, multistream-select 에 따라 Encryption Protocol Negotiation
    • Negotiation 이 잘 되면 그 Encryption Protocol 에서 Connection 을 랩

Swarm Stream Muxer Protocol

1.3.7.6 Multi-StreamMuxer Protocol handled with multistream

Encryption 과 마찬가지로 Stream Muxer 의 Instance 도 외부 Injection 되므로 범위 밖이지만 libp2p 의 이념에서 Stream Muxer Protocol 도 선택할 수 있어야 한다. (multistream을 이용하는 것이 전제된 것으로 예상)

go-libp2p 는 Stream Muxer 로 go-smux-multistream 을 이용하고 있으며 Node.js 는 js-libp2p-switch 에서 Muxer 에 이용되고 있다.

go-smux-multistream 은

  • Stream Muxer Protocol 을 미리 복수 등록
    • 이것이 대응 Protocol
  • Upgrader 에 의해 Connection 이 Upgrade 될 때, multistream-select 에 따라 Stream Muxer Protocol 의 Negotiation
    • Negotiation 이 잘 되면 그 Stream Muxer Protocol 에서 Connection 을 랩

Swarm Stream Muxer Protocol

1.3.7.7 Multi-Stream Protocol handled with multistream

Swarm 범위 밖이지만 이것도 libp2p 의 이념에서 multistream 을 이용하는 것을 예상할 수 있다.

실제로 go-libp2p/p2p/host/basic/basic_host.go 에서 다음과 같이 사용하고 있다.

[Dial ]

func (h *BasicHost) NewStream(ctx context.Context, p peer.ID, pids ...protocol.ID) (inet.Stream, error) {
  ...

  var protoStrs []string
  for _, pid := range pids {
    protoStrs = append(protoStrs, string(pid)) // 연결하고 싶은 Protocol 을 목록으로
  }

  s, err := h.Network().NewStream(ctx, p) // 새로운 Stream 생성
  if err != nil {
    return nil, err
  }

  selected, err := msmux.SelectOneOf(protoStrs, s) // 그 Stream 에 multistream negotiation
  if err != nil {
    s.Reset()
    return nil, err
  }
  selpid := protocol.ID(selected)
  s.SetProtocol(selpid)
  h.Peerstore().AddProtocols(p, selected) // 한 번 연결된 Protocol 은 저장

  return s, nil
}
[Listen ]

// Host 인스턴스 생성시
func NewHost(ctx context.Context, net inet.Network, opts *HostOpts) (*BasicHost, error) {
  h := &BasicHost{
    network:      net, // Swarm 인스턴스
    mux:          msmux.NewMultistreamMuxer(), // go-multistream 인스턴스
    negtimeout:   DefaultNegotiationTimeout,
    AddrsFactory: DefaultAddrsFactory,
    maResolver:   madns.DefaultResolver,
  }
  ...

  net.SetStreamHandler(h.newStreamHandler) // Swarm 에서 Stream 확립했을 때 불리는 handler 등록
  return h, nil
}

// 호출 handler
func (h *BasicHost) newStreamHandler(s inet.Stream) {
  ...

  lzc, protoID, handle, err := h.Mux().NegotiateLazy(s) // 요청을 받고 Negotiation
  took := time.Now().Sub(before)
  ...

  s.SetProtocol(protocol.ID(protoID))
  log.Debugf("protocol negotiation took %s", took)

  go handle(protoID, s) // 사용자가 등록한 handler 호출
}

// 사용자가 handler 등록
func (h *BasicHost) SetStreamHandler(pid protocol.ID, handler inet.StreamHandler) {
  h.Mux().AddHandler(string(pid), func(p string, rwc io.ReadWriteCloser) error {
    is := rwc.(inet.Stream)
    is.SetProtocol(protocol.ID(p))
    handler(is)
    return nil
  })
}
[사용하는 경우]

// Register protocol
host.SetStreamHandler("/echo/1.2", handleStream)

// OpenStream for protocol
s, err := host.NewStream(context.Background(), info.ID, "/echo/1.2")

Swarm Multi-Stream Protocol handler

1.3.7.8 Swarm 정리

  • Transport Layer
    • Protocol 선택은 Swarm 이 Connection 이 설정 될 때 multiaddr 을 보면서 판단
  • Encryption Layer
    • Protocol 선택은 이용 측에 의존
      • 직접 사용하는 경우는 선택할 필요 없음
      • conn-security-multistream 을 사용한 경우 Transport Connection 설치시 multistream Negotiation 에서 선택
  • Stream Multiplex Layer
    • Protocol 선택은 이용 측에 의존
      • 직접 사용하는 경우는 선택할 필요 없음
      • smux-multistream 의 경우 Transport Connection 설치시 multistream Negotiation 에서 선택
  • Application Layer
    • Protocol 선택은 이용 측에 의존
      • multistream 를 이용한 경우 Stream 생성시 multistream Negotiation 에서 선택

각층마다 Protocol 을 Multiple 로 하고 Peer 끼리 합의를 하면서 Protocol 을 결정하는 방향성을 가지고 있는것을 알 수 있다.

1.3.8 Host ( Node )

Swarm 을 내포하고 Network 이외의 Modules 도 통합해 관리하는 역할로 말하자면 Node(Peer) 자체를 나타내는 모델이다. 특별한 사양이 정해져 있는 것은 아니다.

구현

Go 의 경우 go-libp2p-host 에 Interface 가 정의되어 있다.


IPFS 디자인 - Identities

IPFS는 다양한 기능을 담당하는 하위 프로토콜을 조합하여 구성된다.

  • Identities
  • Network
  • Routing
  • Exchange
  • Discovery
  • Objects
  • Files
  • Naming

IPFS 를 구성하는 많은 모듈들은 별도의 프로젝트로 진행되고 있다.

차분히 하나씩 알아보는 시간을 가지고자 한다.

1. Identities

IPFS 의 Node (peer) 는 분산되어 있기 때문에 각 Node 를 식별하기 위해 NodeID 를 발행한다. NodeID 는 공개키를 Cryptographic Hash 함수를 사용하여 얻은 Digest 를 이용한다.

공개키를 활용한 NodeID 생성

type NodeId Multihash
type Multihash []byte
// self-describing cryptographic hash digest

type PublicKey []byte
type PrivateKey []byte
// self-describing keys

type Node struct {
  NodeId NodeID
  PubKey PublicKey
  PriKey PrivateKey
}

difficulty = << integer parameter >>;
n = Node{};
for {
  n.PubKey, n.PrivKey = PKI.genKeyPair()
  n.NodeId = hash(n.PubKey)
  p = count_preceding_zero_bits(hash(n.NodeId))

  if p < difficulty {
    break
  }
}

Node 간 연결은 서로의 공개 키를 교환하고 hash(n.PubKey) == NodeID 검사를 실시하여 Fake 노드를 판단한다. (NodeID 값이 공개키 Hash 값과 같아야 함)

1.1 multihash (self-describing hash format)

자가 기술형 Hash 포멧 (self-describing hash format) 이라고 할 수 있다. Hash Digest 전에 자신의 사용하는 Hash 함수의 ‘타입, 길이’메타 데이터 정보를 포함하는 것이다.

<function_code><digest_length><digest_bytes>

자가 기술형 Hash 포멧의 장점은 Hash 함수가 다른 Node Hash 함수와 통신이 가능한 것이다. Hash 함수가 버전업 했을 경우에도 과거의 Hash 값을 다시 생성할 필요는 없다.

IPFS 에서는 multihash 라는 방식이 취해지고 있다.

github.com/multiformats/multihash

<varint_hash_function_code><varint_digest_size_in_bytes><hash_function_output>
// Binary
  hash    digest                     hash
function   size                     digest
--------  --------  ------------------------------------
00010001  00000100  101101100 11111000 01011100 10110101
sha1      4 bytes   4 byte sha1 digest

multihash 를 활용한 Node 간 통신

현재, IPFS 상에서는 아래의 Hash 함수 타입을 사용할 수 있다.

  • sha2-256
  • sha2-512
  • sha3

count_preceding_zero_bits 함수가 반환하는 값이 일정한 값 이하가 아닌경우 생성되지 않는다.
마치 BitCoin 마이닝의 Nonce 를 찾는 작업과 같다. 시빌 공격 (대량의 ID 를 생성해 네트워크를 빼앗는 공격) 에 대한 대응이라고 생각할 수 있다.

1.2 모듈

1.2.1 키생성

Node.js의 경우는 js-libp2p-crypto 가 제공되고 있다. 브라우저용으로 WebCrypt API에 대한 대응도 하고 있다.

Go의 경우는 go-libp2p-crypto 가 제공되고 있다.

1.2.2 PeerID 생성

PeerID 를 생성하기 전에, 생성한 키의 회전을 높일 수 있는 연구가 필요하다.
예를들어 RSA키를 생성할 경우 공개 키를 RSA DER 형식의 인코딩을 하고 키 정보를 저장하는 protobuf 에서 정의되는 구조체에 포함하여 protobuf 에서 시리얼라이즈한 것을 Base64 에 걸친 문자열로 구성하는 것이다.

RSA, protobuf, Base64 를 활용한 PeerID 생성

Node.js의 경우 js-peer-id 가 생성되어 있다. 실제로는 js-peer-info 가 Peer 추상모델을 작성할 때 호출된다.

Go의 경우는 go-libp2p-peer 가 담당한다.

1.2.3 Peer 및 키 정보 보존

교환된 Peer 정보를 저장하기 위한 모듈도 구성되어 있다.

Node.js 의 경우는 js-peer-book 이 담당한다.
Key=PeerID/Value=PeerInfo 라는 Map 을 On-Memory 에 유지하면서 액세서를 공개하고 있다.

Go의 경우 go-libp2p-peerstore 에서 Interface 를 제공한다.

Peer 정보와 Key 정보의 보존 외에 PeerMetadeta (Peer 마다 추가 정보) 를 보존할 수 있도록 되어 있다. 그 외,

  • 저장정보 CRUD 스레드 세이프
  • Peer 마다 TTL 설정
  • Latency 감시

등의 기능을 가지고 있다.
go-libp2p-peerstore 의 구현은 다음과 같다.

외부 스토리지나 분산 스토리지에 Peer 정보를 보존할 수 있는 기능을 제공하고 있음을 알 수 있다.


IPFS swarm

ipfs 네트워크에서 노드에 대한 연결을 모니터하고 유지 보수를 담당하는 구성요소 입니다. Swarm 명령은 swarm 구성 요소를 조작하는 데 사용됩니다. 기본 형식은 다음과 같습니다:

ipfs swarm <명령어>

다음과 같은 5 개의 하위 명령이 있습니다.

  • addrs: 알려진 주소 리스트 (디버깅에 유용)
    • id bool: 주소별로 노드를 나열하며, 기본값은 false
  • connect <address>: 주소 연결
    • 피어 주소에 대한 새로운 연결
    • 주소 형식은 IPFS 멀티 주소
  • disconnect <address>: 주소 연결 끊기
    • 피어 주소에 대한 연결을 닫음
    • 주소 형식은 IPFS 멀티 주소
    • 연결 해제는 영구적이지 않고 언제든지 다시 연결할 수 있음
  • filters: 작업 주소 필터
    • 현재 필터가 적용된 내용 나열
    • 부속 명령을 사용하여 필터 추가 및 제거
    • 주소 형식은 IPFS 멀티 주소
/ip4/19.168.0.0/ipcidr/16
는
192.168.0.0/16
와 같음

부속 명령 addrm 은 필터를 추가 또는 제거하는 데 사용되지만 프로세스가 다시 시작될 때 초기화 됩니다. 필터는 기본적으로 Swarm.AddrFilters 에 지정된 필터로 필터링 됩니다.

  • peers: 열려있는 피어 연결 리스트
    • v bool: 추가 정보 표시
    • streams bool : 동시에 열려있는 스트림에 대한 각 노드 정보
    • latency bool : 각 피어 대기 시간 정보
$ ipfs swarm peers
/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ
/ip4/178.62.61.185/tcp/4001/ipfs/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3

IPFS object

ipfs 네트워크에서 업로드한 파일 하나하나가 하나의 object 이며, 일종의 DAG 데이터 유형을 가지고 있습니다. object 명령은 ipfs 에서 DAG 객체와 상호 작용하는 데 사용됩니다. 기본 형식은 다음과 같습니다:

ipfs object <명령어> hash

다음과 같은 8 개의 하위 명령이 있습니다:

  • data: 객체의 데이터 부분의 원시 바이트 출력, stdout
    • 출력은 원시 데이터이므로 -encoding 옵션은 출력에 영향을주지 않음
  • diff: 두 object 의 차이점 표시
    • v: 추가 정보 출력
  • get: DAG 노드 가져 오기 및 직렬화, stdout
    • 인코딩 옵션; protobuf, json, xml의 세 가지 데이터 출력 형식 지정
  • links: 출력 대상의 각 분할에 대한 링크
    • v: 헤더 출력
  • new: 제공된 템플릿을 기반으로 새 객체 만들기
    • 새 객체 만들기 템플릿이 제공되지 않으면 기본적으로 빈 객체 생성
  • patch: 기존 DAG (사용자 지정 DAG) 를 기반으로 새 object 생성
    • 다음과 같은 4 개의 부속 명령이 있습니다:
      • add-link <root><name><ref>: 지정된 객체에 링크 추가
        • root: 조정할 노드를 지정하는 hash
        • name: 생성 될 노드의 이름
        • ref: 추가 할 링크
        • -p: 브로커 노드 생성
      • append-data <root><data>: DAG 노드의 데이터 세그먼트에 데이터 추가
        • root: 조정할 노드를 지정하는 hash
        • data: 추가 할 데이터
      • rm-link <root><link>: object 에서 링크 제거
      • set-data <root><data>: object 데이터 세그먼트 설정
  • put: 입력 정보를 DAG object 로 저장하고 hash 출력

  • stat: object 의 상태 제공