1. Gossip 프로토콜 일반 

이더리움의 DEVp2p 네트워킹보다는 비교적으로 간단한 편인 하이퍼레저 패브릭에서의 네트워킹구조 를 살펴보자.
참고로 이더리움의 DEVp2p 에 관련되어 이전에 작성한 글이 있으니 Public 체인에서는 어떻게 하는지 참고 하자.

-> [Ethereum] Node Discovery with Kademlia 
-> [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) 

암튼 둘다 Goosip 을 이용하는데, Gossip 즉 소문이란 무엇인가? 내가 주변 몇사람한테 연예인에 대한 잘못된 소문을 내는 순간에 그들이 또 각자 소문을 내고 이런식으로 내가 전체에게 알리지 않아도 전체가 알게 되는 것을 말하며 특징은 이런것이 있을 수 있겠다..

- 전체에 말하지 않아도 되는 편리함.
- a 라는 사람에게 도착하는 순서가 정해져 있지 않음.  
- 어떤 경로가 끊어져도, a 는 다른 경로를 통해서 전달 받을 수 있다.

- 중복 전달 될 수도 있다. 
- 최악의 상황에선 전달 못 받을 수도 있다. 
- 구라가 횡횡 할 수 있다. 

이런 특징을 그대로 구현한것이 Goosip 프로토콜이다. 근데 좀 룰을 정한게 있는데 

- 한 노드는 부트스트랩 노드를 통해 시작하며, 부트스트랩 노드 각각이 가지고 있는 Alive 노드정보를 합쳐서 셋 구성. 
- 한 노드는 주변 연결된 노드들이 살아있는지 계속 확인 해야한다. 
- 한 노드는 자기가 알고 있는 노드들의 전체 정보를 주변에 주기적으로 알려줘서 전체 네트워크가 현재 상황에 대해 알 수 있게 한다.
- 소문을 낼 때에는 전체노드중 몇개를 랜덤하게 정해서 소문을 퍼트린다.
- 소문을 반복해서 퍼트리지 않는다.전에 받은 소문은 다시 처리하지 않는다.
- 이더리움은 구라를 수용하면서 신뢰를 만들고, 하이퍼레저 패브릭은 인증을 통해서 원천봉쇄한다. 

자 이 아이디어를 머리에 담고 하이퍼레저 패브릭을 살펴보자.

이미 아시는 분도 있겠지만 하이퍼레저 패브릭의 워크플로우를 간단히 설명하면,

1. 클라이언트는 어플리케이션(SDK)를 통해서 Peer 들에게 트랜잭션을 실행 시킨다.
2. Endorsement역할을 하는 이 Peer 들은 체인코드를 실행시키고 장착된 체인코드 로직에 따라서 결과를 내어 다시 클라쪽으로 read/write 셋을 전달 한다.(이 과정에서 장부를 업데이트 하지 않음)
3. 이 결과 셋을 가지고 orderer 서비스에게 순서를 정해서 블록화 해달라고 요청한다.
4. orerer 서비스는 블록화 하면 Peer 들은 이 블록을 가져와서 검증하고 저장한다.

이 과정 중에서 Gossip 프로토콜이 이용되는것이 바로 4번 flow 에서이다. 
즉 orderer 는 모든 peer과 커뮤니케이션을 하는게 아니라, 조직별 대표 peer 하나에게 알리면 이 peer 가 gossip 을 통해 점진적으로 전체로 전달되게 되는 것이다. 각 피어는 전달받은 블록(트랜잭션 뭉치들)을 검증하고 장부(상태DB&블록체인) 에 저장한다. (Gossip은 또한 Peer간 Sync를 맞추는데도 사용된다.)



조금 더 구체적인 위의 그림에서는 5번에 해당한다. 리더피어(그림이 잘못됨. 앵커피어가 아니다) 에게만 전달하면 그것이 소문(블록정보)을 퍼트리기 시작한다. 그림에는 브로드캐스트 처럼 보이지만 저기서 가쉽이 사용되는데, 조직마다 리더피어가 있으며, 그 녀석이 오더러에게서 Pull 하면서 시작된다. 그림에는 구분이 안되어 있지만 추가적으로 하나의 조직이 다른 조직의 Peer 와 연계할 때에 Anchor Peer 를 이용한다. 리더피어=Anchor Peer 로도 설정 할 수도 있다.

2. Gossip 프로토콜 with 하이퍼레저 패브릭 

다시 좀 더 자세히 알아 보자. 위에서 설명했듯이 하이퍼레저 패브릭에서는 트랜잭션 실행 피어와 트랜잭션 정렬 피어 사이에서 업무를 분담해서 CPU부하(고루틴을 통함)  및 네트웍 부하를 처리한다. 이런 분리된 네트워크에서 확장성,보안성, 일관성 등에 대한 처리를 유연하게 하기 위해 패브릭은  가쉽 데이터 전파 프로토콜 을 만들었다. 

각 가십메세지들은 서명되어서 전달되며, 그에 따라 중간에 악의적인 노드의 메세지도 쉽게 확인되며, 가쉽 프로토콜 특성상 늦게 도착하거나, 몇몇 노드들의 네트워크 분단 상황에서도 결국 싱크는 맞춰지게된다.   

패프릭 네트워크에서 가쉽 데이터 전파 프로토콜  주요 3가지 기능으로는 다음과 같다.

  1. 피어 발견 및 채널 멤버쉽을 관리한다. (이용 가능한 피어들을 계속해서 체크함)
  2. 장부에 기록 할 데이터들을 모든 채널 상의 피어들에 전파.싱크가 안맞는 피어들을 확인하여 모자란 블럭 정보들을 계속해서 공급해줌. 
  3. 새로운 피어가 참여하면 peer to peer 로 장부 데이터들을 업데이트 해줌. 

동일한 채널위의 피어들은 메세지를 계속해서 수신하고,주변 피어에 전파하며, 싱크를 맞추게 된다. 이웃피어의 갯수는 설정으로 정해져 있으며, Pull 메커니즘을 따른다. 따라서 메세지가 올 때 까지 기다리는게 아니라, 적극적으로 가지고 오려는 행동을 한다. 채널 상의 각 조직의 리더 피어는 오더러에게 데이터를 가져(Pull)온 후 자신의 조직에 포함된 피어들에게 전파하기 시작한다.

리더 선출 

다음과 같이 2가지 방식이 있다.

  1. Static – 시스템 관리자는 조직마다 하나의 피어를 설정으로 정한다.

    core.yaml: 파일에서 

    peer: gossip: useLeaderElection: false orgLeader: true

    환경 설정에선 이렇게 하면 core.yaml을 덮어 쓴다. 

    export CORE_PEER_GOSSIP_USELEADERELECTION=false
    export CORE_PEER_GOSSIP_ORGLEADER=true
  2. Dynamic – 리드 선출 프로세스에 따라 결정되는데, 리드 피어가 문제가 생기면 각 피어들은 리드 선출과정에 돌입한다. 리드 선출 프로세스는 피어의 메타데이터에 기반하는데 각 피어가 가지고 있는 ID를 정렬해서 낮은 것이 리드가 되는데,  이 과정에서 여러개의 피어가 자신이 리더라고 주장 할 수 있지만 가장 낮은 이름을 가진 피어가 결국 우선권을 가지며  정해 진 시간 동안 선출이 안되면 그 다음 낮은 피어가 자신이 리더라고 주장 할 수 있게 된다.   좀 더 자세히 -> 요기 

3. 앵커(Anchor) 피어 

앵커 피어는 다른 조직들 간의 네트워크 정보를 가지고 있으며,  Service Discovery에 의해 사용 될 수 있다 즉 클라이언트는 네트워크에 누가 있는지 없어졌는지 등에 관한 상태정보를 알아야 한다. 또한 가쉽 커뮤니케이션을 용이하게 하기 위해 사용된다. 즉 하나의 조직에서 다른 조직에 메세지를 전달 할 때 앵커피어에게 전달한다. 체인코드를 호출 할 때 보증을 맡는 피어가 자신의 조직에만 있는 것은 아니기 않나. 그리고 이 말은 앵커피어는 조직 간 커뮤니케이션에 SPOF도 된다는 의미이다.앵커피어에 대한 정보는 채널 설정에서 정의 되어 있다. 참고로 앵커피어가 리더피어 역할을 해 되도되며, 독립적일 수 도 있다. 

Note

1. 각 피어간의 메세지 보안은 TLS 레이어에 의해 작동하며 서명을 요구하지 않는다. 피어들은 그들의 신원서(CA에 의해 부여된 certificates) 로 인증(authenticated)되며, TLS certs(ECerts를 가지고 TCA에 의해 만들어지는)가 사용될 지라도, 가쉽레이어에서 인증되는 것은 피어신원서이다. 장부블록은 오더러 서비스에 의해 서명되며, 채널의 리더피어들에게 전달된다. 

2. 인증(Authentication)은 피어의 "MSP" 에 의해 조정받는다. 피어가 처음으로 채널상에 접속하게 되면,TLS 세션은 멤버쉽 아이덴터티와 엮이는데, 이것은 기본적으로 각각의 피어를 채널과 네트웍상의 멤버쉽으로 인증하는 것이 된다. 

4. Dissenmination protocol

가십 구성요소는 다음 2개의 모듈로 구성된다.

discovery 모듈 - 활성 상태 및 응답하지 않는 피어 세트 유지

communication모듈 - 연결 유지 및 메시지 배포

discovery 모듈은 통신 모듈을 사용하여 자신의 정보를 보내고, 통신 모듈은 discovery 관련된 메세지를 discovery 모듈에 전달.

discovery 프로토콜 흐름 
각 피어는 생성시 부트스트랩 피어 세트 B를 받는다.
각 피어는 알려진 멤버 집합을 나타내는 <id, endpoint, logical_timeestamp> 의 세트 V응답하지 않는 피어의 유사한 세트 H를 가지고 있으며, V[id]는  ID 요소를 가진 피어의 마지막 활성 메시지를 나타낸다.
메시지는 네트워크 CA에서 발급한 Pub(Priv) 키를 이용하여 암호화된 방식으로 서명(검증)할 수 있다.

discovery 프로토콜 정의 

1) H=BV={}
2) 시작할 때 H의 모든 p에 연결을 시도 하고 성공하면 p를 V에 추가하고 통신 모듈을 업데이트.
3) 
각 p(i) 로 부터 그들이 가지고 있는 V 를 통합하여 자신의 V를 구성. 
4) 
V가 변경된 경우 통신 모듈을 업데이트하여 새로 추가된 모든 피어에 대한 연결을 생성하고 응답하지 않는 피어에 대한 연결을 제거
5) 매 T초 마다 
피어 p는 다른피어들(랜덤이지 않을까)에 자신의 alive 메시지를 전달.메시지에는 다음이 포함된다.  

          P(id), P(endpoint), m (inc_number: P가 돌기 시작한 시간, seq# :각 alive메세지마다 하나씩 증가 )
6) 
discovery 세트 V의 각 p 멤버에 대해, Alive 메시지의 마지막 시간을 추적한다. 받은지 5초가지났다면 
         V세트로부터 그 p를 제거 / H세트에 그 p 추가 / 통신모듈에서 커넥션을 종료 
7) 어떤 
피어 p에서 Alive 메시지 m 을 수신 시:
     - m.id이 V에 없으면 추가 
    - m.timestamp.inc_number > V[id].timestamp.inc_number , m.timestamp.seq# > V[id].timestamp.seq# 면 V에 추가 

Communication 프로토콜 흐름 
원장에 전달되지 않은 메시지에 대한 내부 버퍼 유지
피어에서 수신한 각 메시지에는 확인할 수 있는 ID가 있는데 이 ID는 Alive 메시지인 경우 <m.timeestamp, p.id> 또는 원장 배포 블록인 경우 원장 블록의 해시입니다. 어느 쪽이든 프로토콜 통신 프로토콜은 메시지의 ID를 m.id로 참조합니다. - 각 피어는 이전에 발생한 메시지 ID의 세트 R과 수신 시간을 주기적으로 보유하고 5 초 이상 전에 수신 된 오래된 메시지 아이디를 삭제합니다.

 Communication 프로토콜 정의  1) 시간 t에 피어 p(i)로부터 메시지 m을 수신하면 -> 메시지가 Alive 메시지 인 경우 ->discovery 모듈로 전달 2) 이전에 m.id ID를 받은 경우 (R에서 발견할수있음) 메시지를 삭제 3) 그렇지 않다면    3-1)  <m.id, m.t> 를 R 로 추가 
   3-2)  V로 부터 
k = log (| V |) + 3 개의 무작위 피어를 선택 - 각각의 p(i)가 응답하지 않으면 discovery 모듈을 업데이트하여 p(i) 를 H에 추가 - m 을 무작위 피어 p(i) 들에 전파 (모아 보낼 수도)
4) T 초마다 한 번, V의 각 P에 대해 k = log (| V |) + 3 개의 무작위 응답 피어 세트를 선택하고 4-1) V 전체를 p(i) 들에게 보냄.
   4-2) V(p,i), H(p,i)를 얻어서 V(p,i) , H(p,i) 에 있는 각 p를 V,H에 추가한다. 4-3) Send to p your highest ledger originated message sequence number s for which you received before all m s.t ,i < s 4-4) If you have before received a message m(j) that wasn't passed to the ledger yet such that j>s , send to p a bitmap b of all sequence numbers from s to j , And receive back from p(i) a set of messages M(i) and (optionally) its own s , b 4-5) M의 메시지를 내부적으로 저장 4-6) (Optionally) send back to p(i) a crafted M of your own according to p(i) 's s(i) , b(i) sent 5) 사용자 또는 discovery 모듈로부터 메시지 m을 수신하면 : 5-1) <m.id, m.t>를 R에 추가하십시오. 5-2) k = log (| V |) + 3 개의 랜덤 피어 집합을 선택하고 m을 그들에게 보낸다.

이렇게 정리된 내용을 토대로 다음에는 실제 하이퍼레저 패브릭의 gossip 에 관련된 코드를 분석해 보도록 한다. 언제하냐고? 신이 나면~~

개인적으로 간략하게 구현 해 본 블록전파/가쉽프로토콜 바로가기  



레퍼런스:

https://hyperledger-fabric.readthedocs.io/en/release-1.2/gossip.html
https://github.com/hyperledger/fabric/tree/release-1.2/gossip
https://jira.hyperledger.org/browse/FAB-170



서울 서대문구 (80년대 초중반)

편먹기 : 데덴찌, 짱깸보,가위바이보


구슬로 하는 놀이 알롱구리 ,벽치기,홀짝,삼각형, 깔빼기 등

동그란딱지
로하는 놀이 서너가지
, (전쟁높,글씨높) 

네모딱지
로 하는 놀이 (치기 기술 : 바로치기, 옆치기) 

제기
로 하는 놀이 두세가지 (많이 차기, 둥그렇게 모여서 사람 맞추기,제기 발야구)


나무젓가락
이나 먹나 버린 하드봉으로 고무총만들기
,

야구: 짬뽕,들고쳐,짬뽕공으로 포수없이 하는 야구, 

농구
: 올림픽,반코트,올코트,투바운드


축구
: 축구

깡통차기,자치기,망까기나이먹기,다방구,오징어,손들어다섯발,연날리기,눈썰매,왔다리갔다리,풀무치,가재,잠자리잡기젓가락으로 고무줄먹기각종 칼싸움,폭음탄,지랄탄,화약놀이 , 
친구 등에서 하는 말뚝박기,말타기 

등 계절별로 10여가지 놀이가 있었다.
서울 증심권이라 그런지, 개울물,강 같은데서 하는 놀이는 없다.ㅋ


도준이가 크면 같이 해야겠다. ^^




'주인장' 카테고리의 다른 글

블로깅과 책임  (0) 2019.03.04
망치  (0) 2017.10.11
세방낙조  (0) 2017.06.26
2016년 여름 그리고 94년 여름의 추억  (0) 2016.08.22
안산 바위들  (0) 2016.05.06

해석)

C++:  완성은 되지만, 시대에 뒤떨어진 포인터사용,쓰레드사용에 의한 버그로 맛이 간 제작물이 탄생. 그것을 보완하기 위해 덧붙혀진 복잡한 래퍼기술의 복잡성에 의한 구토. (feat, Mordern C++ design)
JAVA: 시작하기 전에 먼저 프레임워크부터 만들어야함. 혹은 다양한 프레임워크(도구)들에 대해서 잘 알아야함. 시작 못함.
JavaScript:  다양하고 많은 오픈소스 라이브러리/프레임워크들이 있지만 멀 선택해야 할지 모르겠다. 그리고 안정성은 개나줘버려.
NoSQL: 쩐다고 말들은 하지만, 실체는 없음. 혹은 자신이 하려는 일에는 맞지 않음. 
Cobol: 너무 오래되서 유지보수 막장. 잘못 건드리다간 엉뚱하게 작동할 확률이 큼. 건드리지 말자. 
Lisp: 코드에 괄호가 너무 많아서 가독성/유지보수 최악. 
C#:  MS에 독점적인 제약이 있으며,  그것이 틀릴지라도 (말인데 낙타) 그렇게 해야함. 커스터마이징 하지마. 
Assembly: 현대의 복잡한 어플리케이션을 구현하긴 불가능하며, 초단순 작업만 가능. 
PHP: 먼가 그럴싸한것을 만든 것 같지만 결국 허술함이라는 복병이 숨어있다가 뒷통수 때림. 


마소는 회장 바뀌고나서 오픈소스화 정책으로 돌아서서~ 

[하이퍼레저 패브릭]  버전(0.6,1.0,1.1,1.2)별 개요 및 차이점 


VER 0.6 

패브릭은 다음과 같은 요소로 이루어져 있다:

1. Peer노드는 ledger 데이터를 유지하고, 체인과 상호작용하려는 클라이어트를 위한 기본 인터페이스를 제공하다. 

  • Validating peers: 트랜잭션을 검증하며, 체인코드를 실행하고 ledger를 업데이트 할 수 있다.  
    Non-validating peers:  기본적으로 같은 역할을 하지만, 검증로직은 없다. 즉 장부저장 및 클라와 인터페이싱 

2. 멤버쉽 서비스는 허가된 peers 와 네트워크상의 유저들을 위한 ACL(access control lists) 을 유지한다. X.509 인증구조를 사용하여 서비스하며, 트랜잭션을 네트워크에 제출하기 위한 TCerts 를 생성한다. TCerts 는 등록된 사용자의 인증서인 ECerts 에 의해 생성된다. 

3. Ledger 데이터는 키-밸류 스토어인  RocksDB 에 저장된다. 
4. 체인코드라고 불리는 스마트컨트렉트는 비지니스로직을 실행하고, 장부상태를 변경시킨다.
5. Docker는 peers 을 호스팅하는데 사용된다. 
6. gRPC는 기본 와이어 프로토콜로 사용된다.
7. 기본적인 PBFT 알고리즘이 컨센서스 메커니즘으로 사용된다. 다른 것을 사용해도 된다. 

 Membership Services

0.6버전 아키텍처에서 멤버십 서비스는 사용자 참여(ECA), 트랜잭션 권한(TCA), 통신권한(TLS-CA) 등의 인증서를 생성, 조회 등을 담당하는 컴포넌트 역할을 맡았다. 각 참여자 피어에서는 블록체인 개념에서 필요한 합의 알고리즘, 체인코드, 이벤트, 원장 관리 등 작업을 수행했다.

하이퍼레저 패브릭0.6버전

하이퍼레저 패브릭0.6버전

주요 멤버쉽 서비스 요소로는 아래와 같은 것들이 있다.

Registration Authority

Assigns registration username & registration password pairs to network participants. This username/password pair will be used to acquire enrollment certificate from ECA.

Enrollment Certificate Authority (ECA)

Issues enrollment certificates (ECert) to network participants that have already registered with a membership service. ECerts are long term certificates used to identify individual entities participating in one or more networks.

Transaction Certificate Authority (TCA)

Issues transaction certificates (TCerts) to ECert owners. An infinite number of TCerts can be derived from each ECert. TCerts are used by network participants to send transactions. Depending on the level of security requirements, network participants may choose to use a new TCert for every transaction.

TLS-Certificate Authority (TLS-CA)

Blockchain Services

블록체인 서비스는 HTTP/2 표준을 기반으로 P2P 프로토콜을 통해서 분산원장을 관리하며 데이터 구조는 해시 알고리즘을 통해 World state를 복제하는 등 관리 하는데 가장 효율적으로 관리할 수 있도록 최적화되어 있다. 디폴트 합의 알고리즘은 PBFT 이며, 필요에 따라 다른 합의 알고리즘 플러그인(Raft, PoW, PoS)을 연결하고 구성 할 수 있다.

Chaincode Services

체인코드 서비스는 Validating 노드에서 안전하고 가볍운 방법으로 체인코드가 실행되도록 보장합니다. 환경은 보안 OS 및 체인 코드 언어, Go, Java 및 Node.js의 런타임 및 SDK 계층을 포함하는 일련의 서명 된 기본 이미지와 함께 “잠긴”보안 컨테이너입니다. 필요한 경우 다른 언어를 사용할 수 있습니다.

운영환경이거나 개발 환경일 경우 다양한 validating peer와 non-validating peer를 이용하여 블록체인 네트워크를 구성해야 합니다. 이 구성에서 non-validating peer는 이벤트 처리 및 REST API 서비스 관리등의 역할을 하게 되는 노드입니다.

MultipleValidatingPeer

아래 그림을 통해 0.6 패브릭에서 일어나는 프로세싱을 한눈에 확인 할 수 있다.



VER 1.0 


2017년 7월에 1.0 버전이 출시되었다. (2016년 9월에 실험버전) 
Orderer / Endorser 
peer/ Committer peerAnchor peer /Channel / Organization (member) 개념이 생겼으며, Ledger 를 저장하는 DB 방식도 바뀜. Membership service가 없어지고, MSP 와 Fabric -CA 서버가 생김. 

이 그림은 IBM Microclass의 세 번째 강의에서 나온 것으로, 원래 단일 피어 노드가 1.0으로 분할되어 피어 (전체피어의 일부분은 endorsement 역할을 한다. 전체피어는 commit 역할을 한다.) 및 컨센서스 강화에 따른 orderer (정렬 노드)로 나뉘어져 있음을 알 수 있다.

보안/허가 관련해서 많은 변경이 이루어 졌다.  

Identity refers to identity management. Because of the importance of identity management, Fabric extracted the original Membership service as a separate module, Fabric-CA. As a non-licensed network, Fabric adopts a digital certificate mechanism to implement identity authentication and permission control. The CA node implements the PKI service, which is responsible for the generation and revocation of identity certificates, etc.

MSP / CSP / Fabric -CA 란?


아래 3개의 그림은 Ledger 저장소/트랜잭션/데이터 저장 메커니즘에 관련 된 것이다. 




VER 1.1 

1. Hyperledger Fabric 1.1 주요변경 사항 
1.1 Rolling upgrade 방식 도입
1.2 이벤트 서비스 추가/변경 
1.3 CouchDB 인덱스
1.4 Javascript chaincode
1.5 TLS communication
1.6 원장 암호화
1.7 Attribute 기반 체인코드 접근 제어 & 체인코드 API를 통한 클라이언트 아이덴티티 확인

2. Hyperledger Fabric-CA
2.1 CRLs (Certificate Revocation List)
2.2 Fabric-CA 재구성 강화

3. Hyperledger Fabric
Nodejs SDK
3.1 Connection Profiles


VER 1.2

  • Private Data Collections: A way to keep certain data/transactions confidential among a subset of channel members. We also have an architecture document on this topic which can be found here.
  • Service Discovery: Discover network services dynamically, including orderers, peers, chaincode, and endorsement policies, to simplify client applications.
  • Access control: How to configure which client identities can interact with peer functions on a per channel basis.
  • Pluggable endorsement and validation: Utilize pluggable endorsement and validation logic per chaincode.


1.1 과 1.2에 대한 차이점에 대해서 IBM에서 발표한 자료 -> http://www-903.ibm.com/kr/2018_upload/IBM_BlockDevDay0915_Session1.pdf


인터넷은 아직 진화 단계의 초기에 있다

좀 충격적인 문장이죠?  아래의 링크 글에 나오는데요. 매우 휼륭한 글로써 (맞는 글로써가 아니라, 생각을 하게 만드는 글) 블록체인에 관심이 없더라도 개발자들이라면 반드시 읽어보시길 바랍니다. 아래 문장도 좋았습니다.


 게임의 규칙이 변하는 것을 걱정하지 않고 그 프로토콜 위에서 비즈니스를 만들 수 있었기 때문이다.

원문) Why Decentralization Matters  
번역본) 왜 탈중앙화가 중요한가? 

음..현재 웹 아키텍쳐(단일지점서비스)에서 완전히 변화된 발상으로 혁신을 꾀하고 있는 분산웹에 관심 있는 분들은 IPFS 에 대해서 읽어보셨으면 좋겠습니다. 블록체인/코인과 직접적인 관계는 없습니다만 분산이라는 사상과 윤활류로써의 역할은 간접적으로 관계가 있습니다. 간단히 말하면, 블록체인 기술 보다는 토렌토 기술의 진화형이라고 생각하면 더 쉽습니다. 

그리고 링크의 글에서 말하는 바와 같이 사실상 3번째 인터넷 시대는 탈중앙화로 흘러 가고 있는것 같습니다.
그것이 성공하도록 많은 사람들이 현재 새로운 아이디어를 계속 해서 시도해보고, 비판하고~ 재밌는 상황입니다. 
그런 시도를 할 수 있게 해주는 것이, 코인판에서 흘러들어오는 자금력이 원동력이 되고 있으며,(물론 이런 자금을 유용하려는 스캠도 많습니다만;
;) 현재 *이더리움* 이 얕은 수(?)를 쓰지 않고 그 어려운길을 헤쳐나가는데 중심을 잡고 있는 상황입니다. 그 이더리움의 정직성(?)을 레이어1이라고하면, 그 기반위에 레이어2,3이 올라가서 응용레벨로까지 발전 할 수 있느냐가 3번째 인터넷시대의 핵심 장벽으로 생각합니다. 사이드체인이라고 하죠. 이더리움에서는 플라즈마라고 합니다. 관련 구현체들은 나오고 있지만  아직 완벽한 해법이 나오진 않은 상태입니다. 


* 참고로 엔터프라이즈형 블록체인(하이퍼레저 패브릭,CORDA) 의 경우는 좀 다른 케이스입니다. 세번째 인터넷에서 매우 중요한 비허가형 즉 아무나 쉽게 참여 가능하고, (참여 대상이 사기꾼인지 아닌지 전혀 신경 쓰지 않음. 이것이 혁명), 능동적으로 변경해 자신의 길을 나아 갈 수 있는 그 핵심 포인트와 반대되는 먹거리를 찾아서 가는 놈이죠. 아무한테나 노출하는 것을 두려워하여 노드들이 허가가 있어야 참여하며, (그렇다고 각 노드들의 완전한 신뢰를 요구하며 작동하는 시스템은 아닙니다.나름 탈중앙화 시스템임.)정확히 누구인지 인증되야 하며, 트랜잭션에 있어서 비공개가 가능한 서비스입니다. 


* 이더리움은 월드오브워크래프트(WOW)를 하다가 블리자드의 독단적 결정에 의해 자신의 흑마법사 캐릭터의 너프를 먹고 빡쳐서 밤새 울다 게임을 접은 경험이 있는 "비탈릭 부테린" 이라는 돈 욕심 없어보이고 수행자 처럼 생긴 젊은 천재에 의해 이끌어 지고 있습니다. (전 고술이 양손을 버리는 순간 마음에서 접었..) 

* 서점에 가면 "플랫폼의 시대" 에 관련된 책들이 많습니다. 이는 현재 중앙화된 플랫폼이 엄청난 힘을 가지고 있기 때문인데, 곧 서점에서 없어질 '과거에는 그랬다' 의 책이 될런지도 모르겠습니다. ㅎㅎ




마지막으로 탈중앙화보다 더 중요한것은 개발자들의 마음을 얻는 것입니다. ^^


현재 엔터프라이즈 블록체인 세계에서 가장 선도하고 있는 플랫폼이라고 한다면 하이퍼레저 패브릭과 CORDA 라고 말 할 수 있을 것이다. 구글링을 통해 살펴보면 개론적인 글들이 있긴 하다. 참고들 하시고~
Comparison of Ethereum, Hyperledger Fabric and Corda
Technical difference between Ethereum, Hyperledger fabric and R3 Corda


                                                                       표(1)
이 글은 조직 구조 특징을 그림으로 간단히 서술 할 예정이다. 일반 블록체인에 대한 이해는 있어야 해서 설명이 이해하기 쉬울지는 모르겠다. @@ 참고로 아래 그림과 같은 트랜잭션 흐름/컨센서스에 관한 대한 설명은 포함하지 않는다. 


(그림1)

그림에서 각 Peer (패브릭에서 노드(서버)는 peers, orderer,CA 등으로 이루어져 있다)는 나중에 설명 할 채널 데이터(Ledger)를 저장하는 곳이라는 것만 알자. 

Hyperledger Fabric vs CORDA 

둘 다 엔터프라이즈 블록체인이다. 즉 어떤 참여자(조직,그룹등)든지 허가에 의해서만 참여 할 수 있다. 채굴이라는 노가다를 하지 않으며 그들의 신원은 분명하다. 별다른것 없을 것 같은 이 두가지 프라이빗 블록체인에서 가장 큰 구조(설계)적 차이점을 보이는 것은 저장하는 데이터(State,Assets,,Ledger,facts)를 어떻게 조직화하고 공유하냐 인데, 이것을 염두해두고 앞으로의 그림을 살펴보자. 

Hyperledger Fabric 
(key concetps: https://hyperledger-fabric.readthedocs.io/en/release-1.2/key_concepts.html)


(그림2)

채널이라는 놈이 보이는가? 이게 CORDA에는 없는 패브릭의 주요 특징이다. 채널은 서로 간에 독립한다. 
즉 서로 다른 블록체인이라고 생각하면 된다. (Orderer 같은 서비스를 공유 할 수는 있다.)

따라서 채널A에서 일어나는 일들에 대해 채널B는 알 수도 없으며 검증할 수도 없다. 그렇게 하려면 소위 인터체인 (여기서는 인터채널?) 같은게 필요한데, 패브릭에서는 그딴 걸 굳이 구현 할 생각이 없는 것 같다.


(그림3)

그림3) 처럼 채널에는 해당 채널에 맞는 역할을 하는 각각의 조직이 포함되어 있으며, 그들은 서로 유기적으로 관계를 맺고 있고, 데이터(State,Assets,,Ledger,facts)도 서로 공유하며,스마트컨트랙트(패브릭에서는 체인코드라고 한다)를 통해 해당 데이터를 조작 할 수도 있게 된다. 이 조작은 MSP 라는 것을 통해 각 조직이 허가 받은 조작만 가능하도록 할 수 있다. 하이퍼레저 패브릭 1.2에서는 각 조직간의 비밀리에 유지 할 수있는 데이터를 관리하는 방식에 대한 기능도 추가 되었다. 

* 채널 안에 조직들 중에 몇몇은 그룹을 짓기도 하는데 이를 콘소시엄이라고 한다. 여기선 생략. 

CORDA 
(key concetps: https://docs.corda.net/key-concepts.html)


그림(4)

패브릭에서는 다른조직들 간의 장부도 같은 채널내 라면 오픈되어 있는 반면에, Corda의 경우는 애초에 참여자(조직,그룹등) 간에 정보를 모두 공통적으로 가지는 방식을 배제하여 설계 하였다. 즉 위의 그림에서 벤다이어그램 식으로 수출기업과 운송업자간의 공통데이터를 수입정책협회가 알 필요가 없다고 생각한 것인데, 애초에 이렇게 설계를 하다 보니, 개별 참여자간의 데이터 Privacy 에 대한 강점이 하이퍼레저 패브릭에 비해 매우 크다고 할 수 있겠다. (서로 지향하는 바가 다르다) 

위에서 각각의 겹쳐지는 조직("수출기업과 운송업체", "수입기업과 운송업체", "수입기업과 수입정책협회")을 떼어내어  그 하나 하나를 패브릭에서의 "채널" 이라고 생각 한다면, Corda 의 경우에는 채널간에도 데이터 이동이 가능하다. 무슨 얘긴가 하면 아래 그림을 보자. 


그림(5)

앨리스와 밥이 하나의 채널을 이루었지만, 밥은 칼과도 채널을 만들어서 데이터 거래를 할 수 있다는 것이다. 
이때 의심이 드는것이 그럼 밥은 앨리스에게도 1000원을 주고, 칼에게도 1000원을 이중지불 하면 어떻게 되냐는 것인데, Corda 에서는 Notaries라는 전체를 관장하는 서비스가 그것을 방지하는 역할을 하고 있다.

즉 Corda 는 소위 채널간의 거래를 Notraries 라는 것을 통해서 해내고 있다고 말 할 수도 있다. 물론 이런 설명은 모두 블록체인이 아닌 Corda를 블록체인 혹은 Fabric의 채널에 맞춰넣어서 하는 설명이긴 하다. (이해하기 쉽지 않을 수 있으리라 생각한다. 이해가 안간다면 모두 글쓴이 탓이다.) 

정리

정리를 하자면, 패브릭은 조직(참여자)이 있으며, 서로 간련 된 조직들 끼리만 비지니스로직을 처리하고 데이터를 공유하는 "채널" 이라는 개념이 있다. 패브릭에는 이런 채널을 여러개 둘 수가 있는데, 보통 하나의 채널로 다 될 것이다. 이때 혹시 외부 채널의 어떤 조직과 상호운영하고 싶을  수가 있다면, 이건 불가하다. 또한 어떤 새로운 조직을 내 채널에 넣고 싶은데, 해당 채널의 일부데이터만 보여주고 싶은 경우도 불가하다. 예를 들어 채팅방에 누구를 초대했는데, 나와 수현이와의 대화만 보이게 하고 싶은게 불가능하다는 얘기이다. 그 채팅방의 전체대화를 공개해야한다. 하지만 패브릭 1.2 에서는 Private data 를 통해  애초에 채널내의 조직들간에 비공개로 모든 비지니스가 이루어지게 했다면, 새로운 조직을 내 채널에 추가해도 그런 염려는 없겠지만, 개인적인 생각으로 그건 좀 어거지가 아닌가 싶다. 패브릭 답지 못하다!! 

코다의 경우 애초에 모든 조직들간의 장부가 서로 격리되어져 있기 때문에, 위 처럼 새로운 조직(참여자)와 커뮤니케이션을 시작하더라도 다른 참여자와 나눈 커뮤니케이션은 감추어 질 수 있다. 

이제 어느 정도 어디에 패브릭과 코다를 사용해야하는지 대략 보일 것이다. 여러 참여자와 새로운 참여자가 복잡하게 얽혀 있을 금융거래 같은 곳, 기본적으로 서로간의 거래를 다른 참여자에게 노출 시키지 말아야하는 곳에서는 코다가 적합 할 것이고, 처음부터 명확한 조직들 간의 관계를 설정 해 두고, 새로운 참여자에게 비밀유지 보다는 그들 간에 신뢰에만 촛점을 맞추고, 유기적인 조직 구성에 중점을 두며, 일정 부분만 비공개로 관리하는 콘소시엄 형태라면 패브릭이 적합 하다고 볼 수 있다. 


부록

Hyperledger fabric 에서 조직을 어떻게 추상화 하고, 실제 네트워크에서 어떻게 구성되는지 살펴보자. 

추상조직구성)

1. 유저들을 포함한 조직을 만든다. (조직을 대표하는 인증서등도 만들어 진다)
2. 조직들을 묶어서 콘소시엄을 만든다.
3. 채널을 만들어서 원하는 조직들을 포함한다.

실 네트워크 구성)

1. 조직은 그것에 해당되는 Peer들을 선택하여 설정한다.
2. 조직이 가질 수 있는 Peer의 갯수는 1개가 될 수도, 여러개가 될 수도 있다.
3. Peer 는 실제 서버에서 돌아가는 서비스이다. (체인코드를 실행/보증/커밋하는 등 다양한 역할을 한다)
4. 조직마다 자신의 Identity 를 책임질 MSP, CA 서비스를 가지고 있다.
5. 모든 조직이 하나의  orderer 서비스를 이용할 수도 있고, 나눌 수도 있다. 
6. ordereing 서비스의 초기화시 채널(조직)들의 제네시스블록을 제공한다. 
7. Node(서버) 한대에 모든것을 다 집어 넣을 수도 있다. 
8. orderer 서비스의 경우 저것이 SPOF 가 될 수도 있기 때문에 주키퍼를 이용해 분산코디네이팅을 해줄 수 도 있다.





서론

이전 글에서는 파이썬 기반으로 분석해 보았는데 이번에는 go-ethereum의 devp2p 를 대상으로 한다. 

이름이 나타내는 것처럼 go-ethereum은 구글에서 만든 go언어 기반인데, 

고 언어.....고 언어.... 코드리딩... 

계획대로 생활하며 모든것에 주기가 붙어 있는 군인의 삶을 쫒는게 쉬울까? 자유롭게 여행다니는 사람의 행적을 쫒는게 쉬울까?
golang 의 장점은 
컴파일속도,실행속도가 빠르고, 멀티코어를 활용하는데 있어서 언어자체적으로 쉽게 사용하도록 지원하는 것이 큰 장점이며, 로직을 구성하는데 있어서의 구현의 자유로움과 단순함(키워드가 별로 없다는 것을 장점으로 내세운다)인데, 역으로 그것이 코드리딩에 있어서는 오히려 굉장히 어려운 요소로 작동한다. 고 언어로 짜여진 코드는 객체지향언어 (자바,파이썬,C++) 로 짜여진 코드에 비해서 경계가 모호하며, 고 언어의 꽃인 고루틴,채널의 범벅으로 인한 이벤트 기반 코드이기에, 시리얼하게 코드를 읽는 습관을 지닌 사람에게는 맨붕 그 자체일 것이다.  (여담으로 자바스크립트가 두세수 위.) 

두가지 예를 들어보면 


첫째연관 짓는것의 단순함&자유로움 (설명에서 구조체,타입,객체를 정확히 구분 하지 않고 혼용하였습니다.)


// conn wraps a network connection with information gathered
// during the two handshakes.
type conn struct {
fd net.Conn (리모트 연결 파일 디스크립터)
transport
flags connFlag
cont chan error // The run loop uses cont to signal errors to SetupConn.
id discover.NodeID // valid after the encryption handshake
caps []Cap // valid after the protocol handshake (리모트가 가진 프로토콜 정보)
name string // valid after the protocol handshake
}

1. 이 코드를 보고 conn 이라는 타입이 어떤 인터페이스와 연관되어 있는지 알 수 없다. 알기 위해서는 이 타입이 구현하고 있는 메소드들을 찾아서 그 메소드 중에 혹시 어떤 인터페이스가 선언 해 둔 메소드가 있는지 확인 해야한다. 즉 자바가 implements 키워드를 사용하는 것 처럼 눈에 띄게 붙어있지 않다. go 는 덕타이핑으로 매우 유연하게 폴리모피즘을 지원 하지만 코드리딩엔 불리하다. 

2. 이 코드를 보고 conn 이라는 객체의 변수로 rlpx 객체의 변수와 메소드를 가지고 있으리 라는 것을 알 수 없다. 여기서 transport 는 rlpx 의 부모 인터페이스 라고 볼 수 있는데, 객체지향도 부모만 보고 자식을 바로 알 수 없는 것은 마찬가지긴 하지만, (즉 자식한테 가봐야 어떤 부모를 가졌는지 알 수 있다.) 문제는 고 언어에서는 자식인 rlpx 구조체로 가 봐도, 그게 transport 인터페이스 상속받은 객체라는 것을 바로 알 수 없다. 1번 처럼 확인을 해봐야 한다.

즉 어떤 타입(객체)가 이 놈도 될 수 있고, 저 놈도 될 수 있는 능력을 자유롭게 해 주고 있으니, 코드를 짤 때는 편할지라도, 읽을 경우는 여기 저기 다 살펴봐야한다. 

둘째쓰레드 생성과 이벤트 기반 코드를 작성하는 것의 단순함&자유로움 


// Server manages all peer connections.
type Server struct {
// Config fields may not be modified while the server is running.
Config

...

// These are for Peers, PeerCount (and nothing else).
peerOp chan peerOpFunc
peerOpDone chan struct{}

quit chan struct{}
addstatic chan *discover.Node
removestatic chan *discover.Node
posthandshake chan *conn
addpeer chan *conn
delpeer chan peerDrop
loopWG sync.WaitGroup // loop, listenLoop
peerFeed event.Feed
log log.Logger
}

for {
scheduleTasks()

select {
case <-srv.quit:
...
case n := <-srv.addstatic:
...
case n := <-srv.removestatic:
...
case op := <-srv.peerOp:
...
case t := <-taskdone:
...
case c := <-srv.posthandshake:
...
case c := <-srv.addpeer:
...
case pd := <-srv.delpeer:
...
}
}

타입(객체) 하나에 채널이 수도 없이 많다. 즉 다른 어떤 경량쓰레드(고루틴)에서 어떤 이벤트가 벌어지는 경우에 한해서 행동하는 방식으로 대부분의 코드가 짜여져 있기 때문에, 거리가 먼 행위들을 연관 지어서 상상 할 수 있는 능력이 더 필요해 진다. 고전적인 C,C++,JAVA 의 경우 코드를 이해하려면 비교적 이어지는 주변의 코드만 집중하면 되었다면, golang 의 경우 전체적으로 조망하는 능력이 더 필요하다고 볼 수 있다. 위에서 switch 처럼 생긴 select 문은 이벤트가 발생하길 기다리는 녀석이다. 이 처럼 go-ethereum의 코드는 생산자(이벤트발생)-소비자(이벤트소비)가 무지하게 복잡하게 얽혀있는 구조를 가진다. (그나마 다행인것은 언어 자체적으로 지원하는 chan 이라는 키워드 덕분에 비동기& 멀티 쓰레딩 코드이긴 해도 순서 관계가 명확하게 보이고 있다. 즉 Mutex 를 사용하지 않고 액터패턴처럼 구현되기 때문에  안정적이게 된다.  C++에 없던 interface 때문에 자바의 객체지향 설계가 더 명확해 지는 것처럼)

사전지식

소스를 온전히 이해하기 위한 많은 사전 지식이 있는데 나열해 보면 아래와 같다.

1. 고언어 (기본 문법/라이브러리 + 고루틴,채널에 대한 체득) 
2. 소켓통신 및 비동기 I/O , Multiplexing  개념
3. ECC 기술들(ECDSA,ECDH),대칭키,공개키,암호화해싱,서명 같은 암호화 기본 
4. Kademlia DHT 
5. RLP 인코딩/디코딩
6. NAT,홀펀칭,UPNP 개념

소스 분석 시작 

1. 조망 - 큰 그림으로 보기 


시작하면 노드를 만들고, 노드 안에서 p2p 패키지의 서버가 돌아간다. devp2p의 영역은 위의 그림에서 p2p 박스에 해당되며, 그 안에 노드 디스커버리와 Rlpx가 있다. 노드 디스커버리는 UDP 프로토콜을 이용하여 노드탐색에 이용되며, 탐색된 노드를 이용하여 실제 TCP 커넥션을 맺고, Encrypt 핸드쉐이크와 프로토콜 핸드쉐이크(위의 eth 프로토콜에 대한 정보 교환) 를 담당하며, 이후의 데이터 교환을 책임지는 역할은 Rlpx 박스에서 한다. 오른쪽의 eth 박스 부분에서는 실제 블록체인에서 하는 일에 대한 데이터 교환에 대한 로직을 담당하고 그것의 입력,출력을 왼쪽의 p2p를 이용하여 처리하게 된다. 지난 글에서 노드 디스커버리와 Rlpx에 대해서 대략 살펴봤기 때문에 이번 글에서는 주로 응용프로토콜과의 인터페이싱 부분(위 그림의 노란 화살표)에 대해서 살펴 볼 것이다.

 p2p 코어계층과 ethereum응용계층과의 관계를 조금 더 명확히 해보면, 이더리움이 시작되면 Node 객체가 생성되는데 

1. 생성되는 과정이 오른쪽인데 노드 내부에 이더리움 서비스가 생성된다. (이 말은 다른 서비스가 생성 될 수도 있다는 의미이다.) 이더리움 서비스는 내부에  프로토콜 매니저를 운용하는데 서브프로토콜을 가지고 있고, 각각 프로토콜의 규칙을 따라서 외부와 통신하기 위한 eth.Peer 객체가 생성 될 준비를 한다. 이 eth.Peer 객체는 아래 2번에서 외부peer와 연결이 되면 Run메소드로 만들어지며 기능을 하기 시작한다. * 이 라인으로는  Protocol 객체를 만든다고 생각하자. (상호 통신 위의  응용 메소드 규약)
  
2. 왼쪽의 p2p 코어층에서는 다른 소켓프로그램들이 그렇듯이 다른 peer 의 connection을 리스닝하고 있다가, conn 객체 (이것은 io 및 rlpx인코딩/디코딩을 책임진다) 와 protocols 인터페이스를 구현한 객체를 만들고 이 둘을 매개변수로 갖는 p2p.Peer 객체를 만든후에 최종적으로 위의 1번에서 eth.Peer 객체를 만들라고 요청하게 되고 서로간의 인터페이싱이 시작된다. 
* 이 라인의 포인트는  conn 객체를 만드는 것으로 생각하자. (상호통신 그 자체) 

2번 라인에서는 최종적으로 protoRW객체가 만들어지는데, 이 객체 안에는 1번 라인의 최종인 Protocol 객체와 2번라인의 최종인 conn객체를 가지고 있다. 이 protoRW는 Peer 객체 안에서 상호통신 작업을 대리한다.

2.  p2p 코어 계층 살펴보기

이번에는 eth 와 인터페이싱 될 p2p 코어 부분을 먼저 살펴보자. (1번 그림에서 Rlpx)

p2p.Peer 는  conn 과 protoRW 를 가지고 있는데. 각자 내부적으로 다른 구조체를 포함하며, 인터페이스를 상속받고 있다. 나중에 응용쪽에서도 peer 객체가 생성되는데, 그 객체는 내부적으로 p2p.peer 를 가지고 통신한다.

conn
- conn 는 순수한 TCP 통신 및 핸드쉐이킹에 촛점이 맞춰져 있고(최종적으로 모든 소켓쓰기,읽기는 이걸 통한다)
- conn 은 리모트 노드와 접속 시작되면 바로 rlpx 를 이용해서 doEnc..(),doProto..() 핸드쉐이킹을 먼저 한 후에, rlpxFrameRW 를 통해 read, writed 를 전담하게 된다. 참고로 devp2p 는 이전 글에서 살펴본 pydevp2p과 다르게 프로토콜 별 공평하게 분배하기 위한 framing 부분이 생략되있다.( 프로토콜별로 정해진 양만큼 번갈아 쓰기 정도로 구현되어 있슴) 이것은 뒤에서 나오겠지만 프로토콜 매니저를 외부가 아니라 gth 패키지에서 관리하고, 서브프로토콜이 gth의 호환목적으로만 구성되는 것만 봐서도 왜 생략되었는지 유추 할 수 있다.

conn 객체가 생성되는 코드는 아래와 같다. 참고로 이후의 모든 소스는 주요 부분위주로 편집되었다.(p2p/server.go)

func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
...
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
err := srv.setupConn(c, flags, dialDest)

return err
}

fd 는 소켓 파일디스크립터이고, newTransprot 는 rlpx 객체이다. 즉 conn 은 rlpx를 이용한다.
SetupConn은 리모트와 접속이 되면 만들어지기 시작되며, srv.setupConn(...) 을 통해서 본격적인 핸드쉐이킹이 시작된다.
여기서 리모트와의 접속은 접속을 할 때(bootnodes 혹은 discovery알고리즘에 따라 발견된 node를 dial을 통해 접속)와 접속을 받을 때(llistenloop 를 통한) 2가지가 있다. 

protoRW
- protoRW 는 프로토콜 정보에 촛점이 맞춰져 있다.
- 응용쪽의 실제 프로토콜 로직에서 데이터를 쓸 경우, 이 protoRW 를 사용한다. 하지만 위에 말했듯이 이 protoRW 도 결국 내부적으로는 conn 을 가지고 있으며 (위 그림을 보면  공통적으로 MsgReadWrite 인터페이스를 상속받고 있는 것을 알 수 있다) 그것을 이용해 최종적인 소켓 입,출력을 하게 된다. 

protoRW 가 conn 을 소유하게 되는 코드는 아래와 같다. (p2p/peer.go)

func matchProtocols(protocols []Protocol, caps []Cap, rw MsgReadWriter) map[string]*protoRW {
....

outer:
for _, cap := range caps {
for _, proto := range protocols {
if proto.Name == cap.Name && proto.Version == cap.Version {
...
result[cap.Name] = &protoRW{Protocol: proto, offset: offset, in: make(chan Msg), w: rw}
...
}
}
}
return result
}

rw 매개변수는 conn 이며, protoRW 객체가 생성 될 때, 마지막에 w 에 할당되는 것을 볼 수 있다.
이 코드는 프로토타입 핸드쉐이크 과정에서 상대 프로토콜(코드에서 caps) 과 내가 가지고 있는 프로토콜(코드에서 protocols)의 이름과 버전을 맞춰보고서, 일치하면 protoRW를 만들어주는 로직이다. 

p2p.Peer 생성

func (srv *Server) listenLoop() {
...

for {
// Wait for a handshake slot before accepting.

fd, err = srv.listener.Accept()

p2p.Server 에서 외부노드에대해 Listening / Accept 를 하고 있다가

func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) error {
...
// Run the encryption handshake.
var err error
if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil {
srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err)
return err
}
...
// Run the protocol handshake
phs, err := c.doProtoHandshake(srv.ourHandshake)
...
c.caps, c.name = phs.Caps, phs.Name
err = srv.checkpoint(c, srv.addpeer)
//
}

접속이 되면 conn 을 셋업하는데, Enc/Proto 핸드쉐이킹을 하여 서로간에 기본 정보를 교환한다.
모든게 잘되면 srv.addpeer 채널을 통해 conn 객체를 보낸다.ㄹㅈㄹ

func (srv *Server) run(dialstate dialer) {
...
running:
for {
scheduleTasks()

select {
case <-srv.quit:
...
case c := <-srv.posthandshake:
...
case c := <-srv.addpeer:
err := srv.protoHandshakeChecks(peers, inboundCount, c)
if err == nil {

p := newPeer(c, srv.Protocols)
...
go srv.runPeer(p)

}

 case c:=<-srv.addpeer: 를 통해 conn 객체를 받은 후에 newPeer(c, svr.Protocols) 로 객체를 만들고 내부적으로 peer.run() 을 실행 하는 srv.runPeer(p) 를 호출.  아래 코드가 peer.run() 이다. 


func (p *Peer) run() (remoteRequested bool, err error) {

go p.readLoop(readErr)
go p.pingLoop()

// Start all protocol handlers.
writeStart <- struct{}{}
p.startProtocols(writeStart, writeErr)
...
}

위 소스 중  startProtocols  내부에서 eth 응용계층의 eth.Peer 객체를 생성하는 proto.Run(p,rw)를 하는 모습을 볼수 있다.

func (p *Peer) startProtocols(writeStart <-chan struct{}, writeErr chan<- error) {
for _, proto := range p.running {
...
go func() {
err := proto.Run(p, rw)
...
}()
}
}

3.  eth 응용 계층 살펴보기

위에서는 네트워킹의 기본이 되는, 소켓을 열고 핸드쉐이킹을 하고, 큰 맥락에서의 프로토콜을 매칭하였다. (큰 맥락이란 이름과 버전을 말한다. 예를들어 eth / 62). 근데 2번에서의 프로토콜을 매칭하기 위해 사용된 자신의 프로토콜 정보는 어디서 나왔을까? 그렇다 그게 여기 3번에서 살펴 볼 내용 중 하나이다. 이제 "자신이 소유한 프로토콜 을 생성하는 부분" "실제 로직에서 그 (eth) 프로토콜이 어떻게 p2p 패키지를 활용" 를 어떻게 하는지 살펴보자.

먼저 eth 객체가 생성되는 모습을 보자. 이것은 p2p.server 가 시작되기 이전에 실행 된다. 즉 다른 노드와 연결되기 이전에 이미 eth 객체가 생성되고, 자신이 가지고 있는 프로토콜 정보를 정리한다는 뜻이다. (app.go)

func RegisterEthService(stack *node.Node, cfg *eth.Config) {
...
err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
fullNode, err := eth.New(ctx, cfg)
if fullNode != nil && cfg.LightServ > 0 {
ls, _ := les.NewLesServer(fullNode, cfg)
fullNode.AddLesServer(ls)
}
return fullNode, err
})
....
}

노드에 서비스들을 등록 해주는데, eth 서비스를 등록해 주며 eth.New 생성함수를 통해 Ethereum 객체가 생성된다. (app.go)

func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
// 체인 디비를 만들고
chainDb, err := CreateDB(ctx, config, "chaindata") // 제네시스 블록을 세팅합니다.
chainConfig, genesisHash, genesisErr := core.SetupGenesisBlock(chainDb, config.Genesis)
// 이더리움 객체 생성.
eth := &Ethereum{
config: config,
chainDb: chainDb,
chainConfig: chainConfig,
eventMux: ctx.EventMux,
accountManager: ctx.AccountManager,
engine: CreateConsensusEngine(ctx, &config.Ethash, chainConfig, chainDb),
shutdownChan: make(chan bool),
networkId: config.NetworkId,
gasPrice: config.GasPrice,
etherbase: config.Etherbase,
bloomRequests: make(chan chan *bloombits.Retrieval),
bloomIndexer: NewBloomIndexer(chainDb, params.BloomBitsBlocks),
}

// 새로운 블록체인을 만들고
eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, eth.chainConfig, eth.engine, vmConfig)

// 블룸인덱서를 시작합니다.
eth.bloomIndexer.Start(eth.blockchain)

// 트랜잭션 풀을 만들고
eth.txPool = core.NewTxPool(config.TxPool, eth.chainConfig, eth.blockchain)

// 프로토콜 매니저를 생성합니다. !! 여기가 우리가 살펴볼 지점입니다. if eth.protocolManager, err = NewProtocolManager(eth.chainConfig, config.SyncMode, config.NetworkId, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb); err != nil {
return nil, err
} // 채굴 객체도 생성해 줍니다.
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
...

return eth, nil
}

Ethereum 객체를 생성해주는 생성자 함수이며, 매우 많은 것들이 여기서 시작됨을 알 수 있다. 그 많은 것들의 결과(트랜잭션,블록등)는 결국 p2p.peer를 통해서 외부와 소통 할 것입니다. (정확히는 p2p.peer가 가지고 있는 protoRW -> conn(rlpx)를 통해서) 

이제 우리가 포커싱을 맞춰야 하는 부분은 NewProtocolManager 이다. (
eth/handler.go)
 "자신이 소유한 프로토콜 을 생성하는 부분"<-- 이것이 바로 여기서 이루어 진다.

func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, networkId uint64, mux *event.TypeMux, txpool txPool, engine consensus.Engine, blockchain *core.BlockChain, chaindb ethdb.Database) (*ProtocolManager, error) {
// 프로토콜 매니저를 만들어 준다.
manager := &ProtocolManager{
networkId: networkId,
eventMux: mux,
txpool: txpool,
blockchain: blockchain,
chainconfig: config,
peers: newPeerSet(),
newPeerCh: make(chan *peer),
noMorePeers: make(chan struct{}),
txsyncCh: make(chan *txsync),
quitSync: make(chan struct{}),
}

// 서브 프로토콜을 할당하기 위한 배열을 초기화 한다.(현재 eth의 프로토콜 버전은 2개이다. 62,63)
manager.SubProtocols = make([]p2p.Protocol, 0, len(ProtocolVersions))
for i, version := range ProtocolVersions {
....
// 서브 프로토콜(eth62,eth63)을 초기화 해서 배열에 추가한다.
manager.SubProtocols = append(manager.SubProtocols, <--- 요기에 서브프로토콜이 들어 간다. ---> )

}

... // fetcher 객체를 만든다.
manager.fetcher = fetcher.New(blockchain.GetBlockByHash, validator, manager.BroadcastBlock, heighter, inserter, manager.removePeer)

return manager, nil
}

위에서 <---- 요기에 서브프로토콜이 들어 간다 --> 에 해당되는 코드가 아래에 있다.

// 프로토콜 매니저가 관리 할 프로토콜 객체를 만든다. 리모트 노드의 프로토콜 정보와 매칭 될 정보이다. p2p.Protocol{
Name: ProtocolName, // 비교될 프로토콜 이름. eth
Version: version, // 비교될 프로토콜 버전. 63,62
Length: ProtocolLengths[i], // 구현된 메세지의 숫자이다. 참고로 63는 17개, 62은 8개 // 나중에 핸드쉐이크가 끝나고, p2p.Peer 와 p2p.protoRW 객체가 매개변수로 들어 와 Run이 호출 되면서 // eth 쪽에 새로운 peer 객체에 포함되며 결합된다. 즉 eth 쪽 Peer 에서 p2p 코어쪽을 활용하게 된다.

Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
peer := manager.newPeer(int(version), p, rw)
select {
case manager.newPeerCh <- peer:
manager.wg.Add(1)
defer manager.wg.Done()
return manager.handle(peer) // <-- 이 코드!!!! (마지막으로 설명할 코드이다.)
case <-manager.quitSync:
return p2p.DiscQuitting
}
},
NodeInfo: func() interface{} {
return manager.NodeInfo()
},
PeerInfo: func(id discover.NodeID) interface{} {
if p := manager.peers.Peer(fmt.Sprintf("%x", id[:8])); p != nil {
return p.Info()
}
return nil
},
}

위에서 프로토콜 길이를 보면 63버전은 18개고 62버전은 8개라고 나오는데, 블록헤더/블록바디/트랜잭션 관련된 데이터에 대한 요청과 처리에 관한 내용이다. 각각의 프로토콜은 아래와 같다. 63버전에서는 Receipts 등에 대한 요청이 추가 되었다.

// Protocol messages belonging to eth/62
StatusMsg = 0x00
NewBlockHashesMsg = 0x01
TxMsg = 0x02
GetBlockHeadersMsg = 0x03
BlockHeadersMsg = 0x04
GetBlockBodiesMsg = 0x05
BlockBodiesMsg = 0x06
NewBlockMsg = 0x07

// Protocol messages belonging to eth/63
GetNodeDataMsg = 0x0d
NodeDataMsg = 0x0e
GetReceiptsMsg = 0x0f
ReceiptsMsg = 0x10

이제 마지막으로 "실제 로직에서 eth 프로토콜이 어떻게 p2p 패키지를 활용" 부분에 대해서 살펴보자.

위에서 p2p.Protocol 객체가 만들어 지면서, 응용계층에 새로운 peer 객체를 만드는데, peer 객체가 만들어 졌다는 이벤트가 채널을 통해서 날라오면 (case manager.newPeerCh <- peer) 프로토콜 매니저는 그 peer 객체를 핸들링하기 시작한다.


return manager.handle(peer) //<-- 이 코드!!!! (마지막으로 설명할 코드이다.)

구체적인 코드로 step in ~

// eth 피어의 라이프 사이클을 관리하기 위한 콜백 함수를 핸들링 한다.
func (pm *ProtocolManager) handle(p *peer) error {

// Execute the Ethereum handshake
var (
genesis = pm.blockchain.Genesis()
head = pm.blockchain.CurrentHeader()
hash = head.Hash()
number = head.Number.Uint64()
td = pm.blockchain.GetTd(hash, number)
) // eth 정보(버전넘버,네트웤ID,Difficulties, head,genesis 블록에 관련된 정보)에 대한 핸드쉐이킹을 한다.
p.Handshake(pm.networkId, td, hash, genesis.Hash()); err != nil {
// p2p 코어(protoRW) 에 자신의 버전을 할당 해 둔다.
rw.Init(p.version)
// 특정 리모트와 연결된 peer 를 등록 해 둔다.
pm.peers.Register(p);
// 다운로드에도 등록 해 둔다.

pm.downloader.RegisterPeer(p.id, p.version, p); // 새로 만들어진 peer 객체에 대해 트랜잭션 동기화를 진행한다. (트랜잭션 전파등) // 여기서 트랜잭션에 관련된 데이터 가 만들어지면, 채널을 통해 전송 할 것이며, // 해당 채널에 대한 이벤트가 일어나길 기다리는 고루틴에서는 p2p 로 전송 할 것이다. (다음 소스 참고)
pm.syncTransactions(p)


// 들어오는 메세지에 대해서 핸들링 할 메인 루프
for {
pm.handleMsg(p)
}
}

위의 syncTranaction 과 같이 어떤 로직에 의해 데이터가 완성되면 채널을 통해 알려 주는데, 해당 채널에서 데이터가 오길 기다리는 소스는 아래와 같다. 아래 소스는 특히 트랜잭션 동기화를 위한 일을 전담하는 고루틴이다. (eth/sync.go, eth/peer.go)

// txsyncLoop takes care of the initial transaction sync for each new
// connection. When a new peer appears, we relay all currently pending
// transactions. In order to minimise egress bandwidth usage, we send
// the transactions in small packs to one peer at a time.
func (pm *ProtocolManager) txsyncLoop() {
....

// 패킷을 만들고 p2p 를 통해 전송 하는 로직
send := func(s *txsync) {
// Fill pack with transactions up to the target size.
size := common.StorageSize(0)
pack.p = s.p
pack.txs = pack.txs[:0]
for i := 0; i < len(s.txs) && size < txsyncPackSize; i++ {
pack.txs = append(pack.txs, s.txs[i])
size += s.txs[i].Size()
}
// Remove the transactions that will be sent.
s.txs = s.txs[:copy(s.txs, s.txs[len(pack.txs):])]
if len(s.txs) == 0 {
delete(pending, s.p.ID())
}

s.p.Log().Trace("Sending batch of transactions", "count", len(pack.txs), "bytes", size)
sending = true
go func() { done <- pack.p.SendTransactions(pack.txs) }() // 전송
}

for {
select {
case s := <-pm.txsyncCh: // 새로운 트랜잭션이 만들어지면
pending[s.p.ID()] = s
if !sending {
send(s) // 패킷을 만들어서 p2p 를 통해 전송한다.
}
...
}
}

func (p *peer) SendTransactions(txs types.Transactions) error {
for _, tx := range txs {
p.knownTxs.Add(tx.Hash())
}
return p2p.Send(p.rw, TxMsg, txs) // p2p 를 통한 전송 !!!
}

마지막으로 아래 코드는 들어오는 데이터에 대한 메세지 핸들링 코드이다. (eth/handle.go)
프로토타입 메세지에 따라서 분기되어 처리되고 있다. 이 부분은P2P랑은 상관없는 부분이니 대충 보자.


func (pm *ProtocolManager) handleMsg(p *peer) error {
msg, err := p.rw.ReadMsg() // protoRW를 통해 읽는다.
...
// Handle the message depending on its contents
switch {
case msg.Code == StatusMsg:
// Status messages should never arrive after the handshake
return errResp(ErrExtraStatusMsg, "uncontrolled status message")

....

case msg.Code == BlockHeadersMsg:
// A batch of headers arrived to one of our previous requests
var headers []*types.Header
if err := msg.Decode(&headers); err != nil {
return errResp(ErrDecode, "msg %v: %v", msg, err)
}
// If no headers were received, but we're expending a DAO fork check, maybe it's that
if len(headers) == 0 && p.forkDrop != nil {
// Possibly an empty reply to the fork header checks, sanity check TDs
verifyDAO := true

// If we already have a DAO header, we can check the peer's TD against it. If
// the peer's ahead of this, it too must have a reply to the DAO check
if daoHeader := pm.blockchain.GetHeaderByNumber(pm.chainconfig.DAOForkBlock.Uint64()); daoHeader != nil {
if _, td := p.Head(); td.Cmp(pm.blockchain.GetTd(daoHeader.Hash(), daoHeader.Number.Uint64())) >= 0 {
verifyDAO = false
}
}
// If we're seemingly on the same chain, disable the drop timer
if verifyDAO {
p.Log().Debug("Seems to be on the same side of the DAO fork")
p.forkDrop.Stop()
p.forkDrop = nil
return nil
}
}

지난 "DevP2P 소스코드 분석  (feat. python)" 글 에서는 노드 디스커버리와 Rlpx 에 대해서 주로 살펴봤다면. 이번 글에서는 eth 응용단 프로토콜과 Rlpx간의 인터페이싱에 대해서 알아 보았습니다. 아마도 여유가 생긴다면 다음 글에서는 블록전송/싱크에 관한 로직을 살펴보고 마지막으로는 SWARM 등의 또 다른 서비스 프로토콜에 대한 로직을 살펴보는 순서대로 글을 작성 할 거 같네요. 

코드가 포함된 긴 글 읽으시느라고 고생하셨습니다.



+ Recent posts