'블록체인' 카테고리의 다른 글
| [하이퍼레저 패브릭] Sawtooth 와의 비교 정리 (0) | 2018.08.28 |
|---|---|
| [하이퍼레저 패브릭] Gossip Protocol (0) | 2018.08.28 |
| [하이퍼레저 패브릭] 버전별 개요 및 차이점 정리 (0) | 2018.08.24 |
| [하이퍼레저 패브릭 vs CORDA] 조직 구조 비교 (0) | 2018.08.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
| [하이퍼레저 패브릭] Sawtooth 와의 비교 정리 (0) | 2018.08.28 |
|---|---|
| [하이퍼레저 패브릭] Gossip Protocol (0) | 2018.08.28 |
| [하이퍼레저 패브릭] 버전별 개요 및 차이점 정리 (0) | 2018.08.24 |
| [하이퍼레저 패브릭 vs CORDA] 조직 구조 비교 (0) | 2018.08.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
[하이퍼레저 패브릭] 버전(0.6,1.0,1.1,1.2)별 개요 및 차이점
전체 릴리즈 노트: https://github.com/hyperledger/fabric#releases
초기 제안 스펙: Protocol Specification
하이퍼레저 백서: https://docs.google.com/document/d/1Z4M_qwILLRehPbVRUsJ3OF8Iir-gqS-ZYe7W-LE9gnE/pub?cm_mc_uid=29910601354314920081869&cm_mc_sid_50200000=1502840355
VER 0.6
패브릭은 다음과 같은 요소로 이루어져 있다:
1. Peer노드는 ledger 데이터를 유지하고, 체인과 상호작용하려는 클라이어트를 위한 기본 인터페이스를 제공하다.
2. 멤버쉽 서비스는 허가된 peers 와 네트워크상의 유저들을 위한 ACL(access control lists) 을 유지한다. X.509 인증구조를 사용하여 서비스하며, 트랜잭션을 네트워크에 제출하기 위한 TCerts 를 생성한다. TCerts 는 등록된 사용자의 인증서인 ECerts 에 의해 생성된다.
3. Ledger 데이터는 키-밸류 스토어인 RocksDB 에 저장된다.
4. 체인코드라고 불리는 스마트컨트렉트는 비지니스로직을 실행하고, 장부상태를 변경시킨다.
5. Docker는 peers 을 호스팅하는데 사용된다.
6. gRPC는 기본 와이어 프로토콜로 사용된다.
7. 기본적인 PBFT 알고리즘이 컨센서스 메커니즘으로 사용된다. 다른 것을 사용해도 된다.
0.6버전 아키텍처에서 멤버십 서비스는 사용자 참여(ECA), 트랜잭션 권한(TCA), 통신권한(TLS-CA) 등의 인증서를 생성, 조회 등을 담당하는 컴포넌트 역할을 맡았다. 각 참여자 피어에서는 블록체인 개념에서 필요한 합의 알고리즘, 체인코드, 이벤트, 원장 관리 등 작업을 수행했다.

하이퍼레저 패브릭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) |
블록체인 서비스는 HTTP/2 표준을 기반으로 P2P 프로토콜을 통해서 분산원장을 관리하며 데이터 구조는 해시 알고리즘을 통해 World state를 복제하는 등 관리 하는데 가장 효율적으로 관리할 수 있도록 최적화되어 있다. 디폴트 합의 알고리즘은 PBFT 이며, 필요에 따라 다른 합의 알고리즘 플러그인(Raft, PoW, PoS)을 연결하고 구성 할 수 있다.
체인코드 서비스는 Validating 노드에서 안전하고 가볍운 방법으로 체인코드가 실행되도록 보장합니다. 환경은 보안 OS 및 체인 코드 언어, Go, Java 및 Node.js의 런타임 및 SDK 계층을 포함하는 일련의 서명 된 기본 이미지와 함께 “잠긴”보안 컨테이너입니다. 필요한 경우 다른 언어를 사용할 수 있습니다.
운영환경이거나 개발 환경일 경우 다양한 validating peer와 non-validating peer를 이용하여 블록체인 네트워크를 구성해야 합니다. 이 구성에서 non-validating peer는 이벤트 처리 및 REST API 서비스 관리등의 역할을 하게 되는 노드입니다.

아래 그림을 통해 0.6 패브릭에서 일어나는 프로세싱을 한눈에 확인 할 수 있다.
VER 1.0
https://medium.com/@ratingtoken/an-introduction-to-hyperledger-fabric-operation-mechanism-eff2542da730
https://blog.michaeldowling.me/the-upcoming-fabric-1-0-release-6271809d023
https://medium.com/wearetheledger/a-technical-exploration-of-the-new-hyperledger-fabric-version-1-0-eca3808f550a
http://HL-Fabric-v1.0-Deep-Dive-20170424.pdf
이 그림은 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
https://medium.com/@yjw113080/hyperledger-fabric-v1-1-release-df631756f6
https://www.hyperledger.org/blog/2018/03/20/hyperledger-fabric-v1-1-released
https://medium.com/wearetheledger/the-new-and-exciting-features-in-hyperledger-fabric-1-1-preview-4261ece3590d
https://www.linkedin.com/pulse/hyperledger-fabric-11-out-whats-new-maciek-j%C4%99drzejczyk
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를
통한 클라이언트 아이덴티티 확인
VER 1.2
1.1 과 1.2에 대한 차이점에 대해서 IBM에서 발표한 자료 -> http://www-903.ibm.com/kr/2018_upload/IBM_BlockDevDay0915_Session1.pdf
| [하이퍼레저 패브릭] Gossip Protocol (0) | 2018.08.28 |
|---|---|
| [하이퍼레저 패브릭] 성능 분석 논문 (0) | 2018.08.24 |
| [하이퍼레저 패브릭 vs CORDA] 조직 구조 비교 (0) | 2018.08.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. Python) (0) | 2018.06.27 |
현재 엔터프라이즈 블록체인 세계에서 가장 선도하고 있는 플랫폼이라고 한다면 하이퍼레저 패브릭과 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 가 될 수도 있기 때문에 주키퍼를 이용해 분산코디네이팅을 해줄 수 도 있다.
| [하이퍼레저 패브릭] 성능 분석 논문 (0) | 2018.08.24 |
|---|---|
| [하이퍼레저 패브릭] 버전별 개요 및 차이점 정리 (0) | 2018.08.24 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. Python) (0) | 2018.06.27 |
| [블록체인] DApp 플랫폼간 장,단점 (3) | 2018.06.12 |
서론
이전 글에서는 파이썬 기반으로 분석해 보았는데 이번에는 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 등의 또 다른 서비스 프로토콜에 대한 로직을 살펴보는 순서대로 글을 작성 할 거 같네요.
코드가 포함된 긴 글 읽으시느라고 고생하셨습니다.
| [하이퍼레저 패브릭] 버전별 개요 및 차이점 정리 (0) | 2018.08.24 |
|---|---|
| [하이퍼레저 패브릭 vs CORDA] 조직 구조 비교 (0) | 2018.08.11 |
| [이더리움 코어] DevP2P 소스코드 분석 (feat. Python) (0) | 2018.06.27 |
| [블록체인] DApp 플랫폼간 장,단점 (3) | 2018.06.12 |
| [이더리움 메모] 트랜잭션의 실전적 종류 구분. (0) | 2018.06.05 |
서론
이 글에서는 이더리움 코어의 중요 축인 P2P에 관해서 분석해 보도록 하는데, 관련 소스는 파이썬 구현체인 pydevp2p 를 대상으로 한다. go 구현체도 있는데 왜 파이썬이냐?
첫째. pydevp2p 는 p2p 에 관해서 독립적인 모듈이다. 즉 이더리움 뿐만 아니라, 분산p2p네트워킹을 하려는 많은 곳에서 재사용 될 수 있다. devp2p와 비슷한 libp2p 는 IPFS에 사용되었으며, 이더리움 SWARM 프로토콜을 이해하는데도 필수적이다.
둘째. 읽기 쉽다. 파이썬은 최고로 가독성이 좋은 언어이다. 개인적으로 모든 개발자들은 커뮤니케이션을 위해 파이썬을 읽을 줄 알아야한다고 생각한다.
사전지식
소스를 온전히 이해하기 위한 많은 사전 지식이 있는데 나열해 보면 아래와 같다.
1. 파이썬 언어 (기본 문법 + 리플렉션 등 고급문법)
2. gevent 라이브러리를 이용한 소켓통신 및 비동기 I/O , Multiplexing 개념
3. ECC 기술들(ECDSA,ECDH),대칭키,공개키,암호화해싱,서명 같은 암호화 기본
4. Kademlia DHT
5. RLP 인코딩/디코딩
6. 기타 (NAT,홀펀칭,UPNP 등 개념)
pydevp2p 소개
pydevp2p 홈페이지에 있는 내용은 대략 이렇다.
"pydevp2p is the Python implementation of the RLPx network layer. RLPx provides a general-purpose transport and interface for applications to communicate via a p2p network. The first version is geared towards building a robust transport, well-formed network, and software interface in order to provide infrastructure which meets the requirements of distributed or decentralized applications such as Ethereum. Encryption is employed to provide better privacy and integrity than would be provided by a cleartext implementation."
pydevp2p 가 제공하는 기능들은 아래와 같은데 소스에서 어떻게 구현 되어 있는지 몇 가지를 살펴 볼 것이다. 사실 개발자라면 이런 추상(?)틱한 소개글 보다는 소스를 직접보는게 훨씬 이해하기 쉬울 것이다.
소스 분석 시작
1. app (app.py)
pydevp2p 는 아래와 같은 2가지 서비스로 이루어져 있다.
프로그램의 시작점인 app.py 를 보면 아래와 같이 두개의 서비스를 등록&시작하는 코드를 볼 수 있다.
# register services
NodeDiscovery.register_with_app(app)
PeerManager.register_with_app(app)
각각의 서비스를 등록하고
def start(self):
for service in self.services.values():
service.start()
각각의 서비스를 시작한다. 이들 서비스는 BaseService 라는 공통부모를 가지고 있는데, Greenlet 이기도 하다.
class BaseService(Greenlet):
Greenlet 은 gevent 라이브러리에서 지원하는 경량 쓰레드라고 생각하면 된다. (golang의 코루틴과 비슷한 면이 있다) 즉 서비스들은 별개의 쓰레드로 작동한다고 보면 된다.
2. NodeDiscovery
노드디스커버리는 처음 어플리케이션이 실행 될 때, 주변의 연결될 노드들의 정보를 찾고, 다른 노드들을 위해 (다른노드들의 노드검색을 위해) 자신과 가까운 노드들의 정보를 저장해 놓는 곳에 대한 알고리즘을 다룬다. kademlia DHT 를 알고 있다고 가정한다. 모르면 예전에 썼던 글을 참고하라. [Ethereum] Node Discovery with Kademlia
클래스 구성은 대략 위와 같으며 대락 설명하면
1. NodeDiscovery 는 pydevp2p 에서 제공하는 서비스의 일종이다.
2. NodeDiscovery 서비스는 노드를 찾기위한 DiscoveryProtocol 를 사용하는데, 구체적으로 KademliaProtocol 를 사용한다.
3. NodeDiscovery 서비스는 DiscoveryProtocol 를 통해 발생하는 명령들을 최종적으로 UDP 를 이용하여 전송하고 받는다.
4. DiscoveryProtocol 는 하위 프로토콜이 무엇이든지 간에 (여기서는 Kademlia 지만) RLP 를 통해 전송/수신 메세지에 대해 인코딩/디코딩하고, 암호화 한다.
5. 모든 노드디스커버리 프로토콜이 상속받아야하는 (하지만 여기서는 kademlia 에 종속된~ 디자인 실패?) WireInterface 는kademlia 에서 사용되는 주요 프로토콜을 선언해두고 있다. (send_ping,send_pong,send_fine_node,send_neighbours)
6. KademliaProtocol 은 kademlia 노드디스커버리를 구현하며 RoutingTable 를 관리한다. kademlia DHT 의 모든 구현이 되있는것은 아니고, 토렌트 같은 데이터를 찾고 저장하는 부분은 제외되있다.
7. RoutingTable 은 내가 가까운 노드들을 많이 담고있고, 먼 노드들은 적게 담고 있는 테이블이다.
8. KBucket 은 RoutingTable 의 한개의 로우에 해당하며, 개수는 지정하기 나름이다. (보통 16개)
9. Node는 하나의 peer 를 나타내며, xor 연산을 하기 위한 id 를 가지고 있다. 이 노드를 상속받아서 더 구체적인 정보(ip,port,reputation등)를 가지고 있는 노드도 있다.
주요 소스를 살펴보자. 녹색 글짜로 주석을 달아 놓았으며 빠른 이해를 위해, 예외등을 삭제했다.
2-1. NodeDiscovery 의 start (discovery.py 에 존재)
class NodeDiscovery(BaseService, DiscoveryProtocolTransport):
// 노드디스커버리를 위한 port 30303 을 설정한다.
default_config = dict(
discovery=dict(
listen_port=30303,
listen_host='0.0.0.0',
),
node=dict(privkey_hex=''))
// 생성자에서 DiscoveryProtocol 객체를 생성한다.
def __init__(self, app):
BaseService.__init__(self, app)
self.protocol = DiscoveryProtocol(app=self.app, transport=self)
// 리모트 노드에 message 를 전송한다. 전송시에는 UDP server 를 이용한다.def send(self, address, message):
self.server.sendto(message, (address.ip, address.udp_port))
// 리모트 노드에서 받은 message 를 kademlia 프로토콜 객체로 건네 준다. // 즉 비지니스 로직은 kademlia 프로토콜 객체에 있고, 이 NodeDiscovery 는 전달자,조정자의 역할이다.def receive(self, address, message):
self.protocol.receive(address, message)// NodeDiscovery 를 시작한다. // 1. upnp 설정을 하여 nat에서도 사용되게 한다. // 2 DatagrameServer 를 시작한다 // 3. kademlia protocol 을 이용하여 설정파일에 기록되있는 노드를 통해 bootstrap한다.
def start(self):
ip = self.app.config['discovery']['listen_host']
port = self.app.config['discovery']['listen_port']
self.nat_upnp = add_portmap(port, 'UDP', 'Ethereum DEVP2P Discovery')
self.server = DatagramServer((ip, port), handle=self._handle_packet)
self.server.start()
super(NodeDiscovery, self).start()
// bootstrap 시작한다.
nodes = [Node.from_uri(x) for x in self.app.config['discovery']['bootstrap_nodes']]
if nodes:
self.protocol.kademlia.bootstrap(nodes)
2-2. KademliaProtocol 의 bootstrap (kedemlia.py 에 존재)
// 가장 먼저 부트스트랩 노드를 일단 라우팅 테이블에 추가한다. (라우팅 테이블은 계속 업데이트 된다) // 그 후에 find_node 를 통해 자신과 가까운 노드들의 정보를 물어 본다. // find_node 의 첫번째 인자는 자신이고, 두번째는 부트스트랩 노드이다. // 즉 부트스트랩 노드에게 자신의 노드와 가까운 노드를 찾아 달라는 것이다.
def bootstrap(self, nodes):
for node in nodes:
if node == self.this_node:
continue
self.routing.add_node(node)
self.find_node(self.this_node.id, via_node=node)
이어서 find_node 를 살펴보자.
// via_node 에게 tergetid 와 가까운 노드를 찾아 달라는 것이다. 만약 특정 via_node가 없으면 // 자신의 라우팅테이블에서 검색해서 가까운 노드들에게 모두 find_node를 시키며, // via_node가 있으면 , 리모트 노드에게 물어본다. // 여기서 find_requests 는 일정시간동안 응답이 없으면 무효화하기 위함이다.
def find_node(self, targetid, via_node=None):
self._find_requests[targetid] = time.time() + k_request_timeout
if via_node:
self.wire.send_find_node(via_node, targetid)
else:
self._query_neighbours(targetid)
실제 메세지를 보내는 send_find_node 는 암호화 및 rlp 인코딩 후 UDP 를 사용할 것이다. 확인해보자.
// 해당 메세지를 packing 하여 UDP 로 보낸다. 여기서 .packing이 바로 rlp 인코딩이다. def send_find_node(self, node, target_node_id):
message = self.pack(self.cmd_id_map['find_node'], [target_node_id])
self.send(node, message)
rlp 인코딩 및 무결성 체크 데이터를 통해 최종 메세지 생성 (여기서는 비밀키를 통해 암호화는 하지 않는다. 나중에 peermanager 서비스에서는 ECDH 키교환에 의한 비밀키를 생성하여 암호화하는 부분을 볼 수 있을 것이다)
//find_node 에 해당하는 cmd_id 와 실제 보내질 메세지를 인자로 받는다. // rlp.encode 를 통해 payload 를 인코딩한다. // 인코딩된 데이터를 sha3 로 서명하고 무결성을 위하여 MDC를 만든다. // 최종적으로 MDC + 서명 + CMD_ID + 실제 데이터 가 보내어진다.
def pack(self, cmd_id, payload):
cmd_id = str_to_bytes(self.encoders['cmd_id'](cmd_id))
expiration = self.encoders['expiration'](int(time.time() + self.expiration))
encoded_data = rlp.encode(payload + [expiration])
signed_data = crypto.sha3(cmd_id + encoded_data)
signature = crypto.sign(signed_data, self.privkey)
mdc = crypto.sha3(signature + cmd_id + encoded_data)
return mdc + signature + cmd_id + encoded_data
query_neighbours 를 살펴보자. 자신의 라우팅 테이블에서 주어진 node_id와 그나마 가장 가까운 노드들에게, 자신과 진짜로 가까운 노드들이 또 있는지 찾아달라고 부탁한다.
// 라우팅테이블의 neighbours 메소드를 호출하여 가까운 것들을 찾아서 모두에게 find_node를 부탁한다. def _query_neighbours(self, targetid):
for n in self.routing.neighbours(targetid)[:k_find_concurrency]:
self.wire.send_find_node(n, targetid)
마지막으로 neighbours 는 이렇다.
// 일단 인자로 들어온 node와 가장가까운 버킷에서 가장 가까운 노드부터 채워넣는다. // k_bucket_size의 2배만큼~ 논문에는 k_bucket_size 는 16으로 되어있으며, 이더리움에서도 16을 쓴다. def neighbours(self, node, k=k_bucket_size):
nodes = []
for bucket in self.buckets_by_id_distance(node):
for n in bucket.nodes_by_id_distance(node):
if n is not node:
nodes.append(n)
if len(nodes) == k * 2:
break
return sorted(nodes, key=operator.methodcaller('id_distance', node))[:k]
Nodediscovery는 나름 단순하므로 나머지 부분은 각자 소스를 보면 되리라 본다.
3. PeerManager
PeerManager는 프로토콜 별로 framming 하는 부분이 소켓 프로그래밍에 있어서 꽤 교육적이고, 리플렉션을 통해 다중 프로토콜을 유연하게 확장 하려는 모습이 돋보여서 나름 읽어볼만하다. 개발자에게 오픈소스는 무협지 같은 소설 아니던가. ~
클래스 구성은 대략 위와 같으며 설명하자면
1. PeerManager도 NodeManager와 마찬가지로 pydevp2p 에서 제공하는 서비스의 일종이다.
2. PeerManager 는 StreamServer 를 통해 발생하는 명령들을 최종적으로 TCP를 이용하여 전송하고 받는다.
3. PeerManager 서비스는 peer 들과 연결된 후에 각각 peer 객체를 생성해서 개별적으로 통신하게 한다.
4. BaseProtocol 은 모든 프로토콜이 가져야할 공통적인 메소드를 가지고 있으며, 하위 객체에서 생성하는 각각의 커맨드 프로토콜의 create/send/receive/receive_callback 메소드를 자동적으로 만들어 주는 리플렉션 기봅이 들어가 있다.
5. P2PProtocol 에는 핸드쉐이킹을 통해 상호 프로토콜 정보와 암호정보를 교환하는 프로토콜과 ping,pong 을 통해 연결을 확인하는 명령이 있다. 이 모든것은 MultiplexedSessiion 을 통해서 한다. rlp 인코딩을 한 메세지를 전달함
6. MultiplexedSession 을 통해서 비동기 i/o 를 처리해준다. 즉 read/write 에 대해 각각의 경량쓰레드를 만든 후에 해당 이벤트가 없는 경우 양보한다. read/write 과 RlpxSession 를 관리한다.
7. 또한 MultiplexedSession 을 통해서 보내야할 데이터를 조각낸다. 조각내는 이유는 소켓버퍼의 양(window size)에 맞추기 위함인데, 프로토콜 별로 조각, 우선순위별 조각등이 각각 조합되어 max_window_size 만큼의 패킷을 완성하게 된다. 해당 패킷은 StreamServer 를 통해 보내어진다. 즉 아래 요구사항을 만족.
8. RlpxSession 에서는 RLP 를 통해 전송/수신 메세지에 대해 인코딩/디코딩하고, 상호동일하게 만들어진 비밀키를 통해 암호화 한다. MultiplexedSession 은 이 부분에 대해서 RlpxSession을 이용한다.
9. 위의 다이어그램에는 나오지 않았지만 커넥션이 맺어지고, hello를 하여 핸드쉐이크(프로토콜 및 비밀키 교환)를 완료하고 난 후 부터는 ConnectionMonitor 클래스를 통해서 ping,pong 을 하여 커넥션을 모니터링 한다.
간단히 정리하면 P2PProtocol 은 상위단의 비니지스 로직이고, MultiplexedSession 은 하위단의 패킷 조작자이다.이 모든것을 Peer가 컨트롤하며, 최종 메세지는 PeearManager가 가지고 있는 TCP 소켓(StreamServe)r을 통해서 보내어진다.
주요 소스를 살펴보자. (프로그램이 시작되고 나서 순서대로/몇몇 예외나 로직들이 생략되었다.)
3-1. PeerManager (peermanger.py 에 존재)
def start(self):
# NAT 문제를 해결하기 위한 upnp 세팅.
self.nat_upnp = add_portmap(
self.config['p2p']['listen_port'],
'TCP',
'Ethereum DEVP2P Peermanager'
)
# TCP 서버의 listening 핸들러 설정. 새로운 접속이 들어오면 호출된다.
self.server.set_handle(self._on_new_connection) # TCP 서버 시작.
self.server.start()
# bootstrap 시작 (이미 하드코딩되어 있는 시작노드에 접속한다)
gevent.spawn_later(0.001, self._bootstrap, self.config['p2p']['bootstrap_nodes']) # discovery 시작 (새로 접속할 노드를 선택하기 위해 kademlia routing table 을 참조한다)
gevent.spawn_later(1, self._discovery_loop)
기본적으로 TCP 서버를 시작하고 있으며, 리모트 노드와 접속하기 위한 위한 경량 쓰레드를 각각 시작한다.
다음으로 disovery_loop 를 따라가 보자.
def _discovery_loop(self): # kademlia node discovery 가 어느정도 완성 될 때 까지 대기~
gevent.sleep(self.discovery_delay) # 이제 부터 계속 접속할 노드를 찾는 과정을 반복한다.
while not self.is_stopped:
# 이제 부터 계속 접속할 노드를 찾는 과정을 반복한다.
num_peers, min_peers = self.num_peers(), self.config['p2p']['min_peers'] # 접속할 노드를 선택하기 위해 미리 채워두었던 kademlia 프로토콜을 참조한다.
kademlia_proto = self.app.services.discovery.protocol.kademlia # 접속할 최소peer 수보다 많을 때 까지 추가한다.
if num_peers < min_peers: # 접속할 노드를 선택하기 위해 랜덤으로 node id 하나를 만든다.
nodeid = kademlia.random_nodeid() # kademlia routing table 에서 가까운 노드들을 가져온다.
kademlia_proto.find_node(nodeid) # fixme, should be a task
neighbours = kademlia_proto.routing.neighbours(nodeid, 2)
# 가져온 노드들 중에 노드 하나를 선택한다. 이 무작위로 선택한 노드와 연결될 것이다.
node = random.choice(neighbours)
# 내 public key를 구해서
local_pubkey = crypto.privtopub(decode_hex(self.config['node']['privkey_hex']))
# 선택된 노드와 연결한다. (그 노드에 내 public key 를 보낼 것이다.
self.connect((node.address.ip, node.address.tcp_port), node.pubkey)
Node discovery 프로토콜에서 채워놓은 라우팅테이블에서 가져온 노드들중에 랜덤으로 하나 선택하여 연결한다.
모든 노드들이 이렇게 연결하기 때문에, 토폴로지와 상관없이 골고루 연결될 수 있을 것이다. 아래에 해당된다.
다음으로 해당 노드와 연결하는 부분을 자세히 살펴보자.
def connect(self, address, remote_pubkey):
# 먼저 gevent 소켓통신 라이브러리에서 제공하는 create_connection 으로 연결정보(소켓디스크립션)를 얻는다.
connection = create_connection(address, timeout=self.connect_timeout) # 이제 해당 노드와 1대1로 통신하기 위한 Peer 객체를 생성한다.
self._start_peer(connection, address, remote_pubkey)
return True
다음으로 _start_peer 를 살펴보자.
def _start_peer(self, connection, address, remote_pubkey=None): # 이제 해당 노드와 1대1로 통신하기 위한 Peer 객체를 생성한다.
peer = Peer(self, connection, remote_pubkey=remote_pubkey)
# peer 경량 쓰레드를 시작한다.
peer.start()
Peer 객체를 시작하고 있다. 인자로는 연결된 상대노드의 public key 정보를 넣어준다. 참고로 이 키는 노드디스커버리 프로토콜에서 얻어졌다. 이제 Peer 객체를 자세히 살펴본다.
3-2. Peer (peer.py 에 존재)
아래는 peer 객체의 생성자이다. 생성자에서는 MultiplexedSession 을 이용하여 리모트 노드와 핸드쉐이크를 통해 상호 신뢰 할 수 있는 연결정보를 확보해 놓는데 이용한다.
def __init__(self, peermanager, connection, remote_pubkey=None):
super(Peer, self).__init__()
self.is_stopped = False
self.hello_received = False
self.peermanager = peermanager
self.connection = connection
self.config = peermanager.config
self.protocols = OrderedDict()
privkey = decode_hex(self.config['node']['privkey_hex']) # P2P Protocol 의 get_hello_packet 클래스메소드를 통해 hello 패킷을 만든다. 이 패킷은 rlp 인코딩되어 리턴된다.
hello_packet = P2PProtocol.get_hello_packet(self) # MultiplexedSession 을 통해서 보내어 질 최종 메세지를 만들기 위한 각종 작업을 한다.(예를들어 framing) # 또한 앞으로 교환정보를 암호화할 비밀키 생성 및 프로토콜 교환을 위한 작업도 담당한다.
self.mux = MultiplexedSession(privkey, hello_packet, remote_pubkey=remote_pubkey)
3-3. MultiplexedSession(muxsession.py 에 존재)
아래는 muxsession 객체의 생성자이다. 내부에 ingress,egress 큐가 존재하여 multiplexing 작업을 할 수 있으며, RLPxSession 객체를 통해 초기 암호화/프로토콜 공유 핸드쉐이킹을 할 수 있게 된다. 또한 멀티프로토콜 끼리 공평하게 패킷공간을 차지하게 하기 위한 framing 도 시작된다.
def __init__(self, privkey, hello_packet, remote_pubkey=None): # 내가 시작한 연결인가? 누군가에게요청되어 온 연결인가? # 내가 시작한 연결이라면 노드디스커버리 프로토콜에서 이미 얻어진 상대 public key를 가지고 있을 것이다.
self.is_initiator = bool(remote_pubkey)
self.hello_packet = hello_packet # 나가는 메세지에 대한 큐
self.message_queue = gevent.queue.Queue() # 들어오는 메세지에 대한 큐
self.packet_queue = gevent.queue.Queue() # 암호화/무결성화 에 사용되는 ECCx 객체 생성
ecc = ECCx(raw_privkey=privkey) # 비밀키/토큰 공유를 위한 RLPxSession 객체 생성
self.rlpx_session = RLPxSession(ecc, is_initiator=bool(remote_pubkey))
# 상대노드에게 msg 를 보낸다. (초기 핸드쉐이킹을 위한)
if self.is_initiator:
self._send_init_msg()
이제 _send_init_msg 를 살펴본다.
def _send_init_msg(self): # RLPxSession 객체를 통해 인증메세지를 만든다. 이 메세지는 아주 많은 정보가 포함되는데 # 아래와 같다. # auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \ self.initiator_nonce + ascii_chr(flag)
auth_msg = self.rlpx_session.create_auth_message(self._remote_pubkey) # 인증메세지를 암호화 한다.auth_msg_ct = self.rlpx_session.encrypt_auth_message(auth_msg) # 암호화된 메세지를 멀티플랙싱을 위한 egress 큐에 넣는다.
self.message_queue.put(auth_msg_ct)
마지막으로 핸드쉐이킹 부분을 살펴보자. 노드에서 초기 핸드세이크를 할 경우에는
add_mesasge 메소드가 add_message_during_handshake 로직을 따르고, 핸드쉐이크가 끝난 후에는
add_message_post_handshake 로직을 따르게 된다.
핸드쉐이킹은 전반적으로 아래와 같으며,
# 핸드쉐이킹 도중에는 받은 메세지를 이렇게 처리한다.
def _add_message_during_handshake(self, msg):
session = self.rlpx_session # 내가 먼저 요청한 연결이라면
if self.is_initiator:
# auth 요청보낸것에 대한 답변인 msg 를 디코드 한다.
rest = session.decode_auth_ack_message(msg) # 리모트에서 넘어온 정보를 토대로 최종적인 상호 암호화 정보를 세팅하게 된다. 앞으로는 구축된 이 정보를 통해 # 상호간 암호화/무결성화된 통신을 하게 될 것이다.
session.setup_cipher()
if len(rest) > 0: # add remains (hello) to queue
self._add_message_post_handshake(rest)
#핸드쉐이킹 요청을 받은 것이라면 else:
# 요청된 auth 정보를 디코딩한다.
rest = session.decode_authentication(msg) # 대답해줄 ack 메세지를 준비한다.
auth_ack_msg = session.create_auth_ack_message() # 대답해줄 ack 메세지를 암호화 한다.
auth_ack_msg_ct = session.encrypt_auth_ack_message(auth_ack_msg) # 전송 큐인 message_queue에 넣는다.
self.message_queue.put(auth_ack_msg_ct) # 리모트로 부터 받은 auth 정보를 토대로 cipher 를 세팅한다.
session.setup_cipher()
self.add_message = self._add_message_post_handshake # 핸드쉐이크 종료후 add_messsage 메소드 # 암호화를 위한 핸드쉐이크가 끝나면 이제 프로토콜을 맞춰보는 핸드쉐이크가 시작된다. self.add_packet(self.hello_packet) add_message = _add_message_during_handshake # 초기 add_messsage 메소드 # 핸드쉐이킹이 끝난 후에는 아주 단순하게 그냥 전달받은 msg 를 디코드하여 수신큐인 packet_queue에 넣는다. def _add_message_post_handshake(self, msg): for packet in self.decode(msg): self.packet_queue.put(packet)
첫번째 핸드쉐이킹은 암호화를 위한 정보공유이다. ECDH 를 이용하여 양쪽 노드끼리 동일한 비밀키를 만든다.
리모트에게 보내 줄 auth 메세지를 만드는 아래 함수를 보자.
def create_auth_message(self, remote_pubkey, ephemeral_privkey=None, nonce=None):
if not self.ecc.is_valid_key(remote_pubkey):
raise InvalidKeyError('invalid remote pubkey')
self.remote_pubkey = remote_pubkey
ecdh_shared_secret = self.ecc.get_ecdh_key(remote_pubkey)
token = ecdh_shared_secret
flag = 0x0
self.initiator_nonce = nonce or sha3(ienc(random.randint(0, 2 ** 256 - 1)))
assert len(self.initiator_nonce) == 32
token_xor_nonce = sxor(token, self.initiator_nonce)
assert len(token_xor_nonce) == 32
ephemeral_pubkey = self.ephemeral_ecc.raw_pubkey
assert len(ephemeral_pubkey) == 512 / 8
if not self.ecc.is_valid_key(ephemeral_pubkey):
raise InvalidKeyError('invalid ephemeral pubkey')
# S(ephemeral-privk, ecdh-shared-secret ^ nonce)
S = self.ephemeral_ecc.sign(token_xor_nonce)
assert len(S) == 65
# S || H(ephemeral-pubk) || pubk || nonce || 0x0
auth_message = S + sha3(ephemeral_pubkey) + self.ecc.raw_pubkey + \
self.initiator_nonce + ascii_chr(flag)
assert len(auth_message) == 65 + 32 + 64 + 32 + 1 == 194
return auth_message
이렇게 나의 암호화 정보에 대한 패킷을 만들어 보내면, 상대방은 이것을 디코딩하여 자신에게 적용하고, 다시 나에게 자신의 정보를 ack 로 보내주면 나 역시 상대방의 정보를 이용하여 암호화 토큰을 완성시킨다. 솔직히 이렇게 까지 감싸고 감싸야 하는지에 대해서는 갸우뚱 하다.
두번째 핸드쉐이킹은 상호 적용 가능한 wire protocol 에 대한 정보교환을 한다. 기본적인 와이어 프로토콜에는 P2PProtocol 이 있다.
@classmethod
def get_hello_packet(cls, peer):
"special: we need this packet before the protocol can be initalized"
res = dict(version=cls.version,
client_version_string=peer.config['client_version_string'],
capabilities=peer.capabilities,
listen_port=peer.config['p2p']['listen_port'],
remote_pubkey=peer.config['node']['id'])
payload = cls.hello.encode_payload(res)
return Packet(cls.protocol_id, cls.hello.cmd_id, payload=payload)
리모트 노드에 내 정보를 보내 준다. capabilities 에 내가 제공하고 있는 와이어 프로토콜의 정보가 담긴다.
그 후에 리모트로 부터 받은 상대방의 capabilities (와이어프로토콜집합)을 등록하고, 기본 프로토콜(P2PProtocol) 이외의 서비스가 서로간에 존재한다면 그것에 대한 서비스요청을 한다. 이렇게 추가된 프로토콜은 여러 프로토콜이 공평하게 메세지를 전달할 수 있도록 Framming 전략에 따라 패킷을 처리된다. 즉 공평하게 큐에 넣어지고, 균등하게 대역폭이 제공된다.
def receive_hello(self, proto, version, client_version_string, capabilities,
listen_port, remote_pubkey):
self.remote_client_version = client_version_string
self.remote_pubkey = remote_pubkey
self.remote_capabilities = capabilities
# 리모트 서비스를 등록한다.
remote_services = dict()
for name, version in capabilities:
remote_services[name].append(version) # p2p protocol 은 기본적으로 있기 때문에, 다른 서비스가 상호간에 존재한다면 그것에 대한 접속 요청.
for service in sorted(self.peermanager.wired_services, key=operator.attrgetter('name')):
proto = service.wire_protocol
if proto.name in remote_services:
if proto.version in remote_services[proto.name]:
if service != self.peermanager: # p2p protocol already registered
self.connect_service(service)
else:
log.debug('wrong version', service=proto.name, local_version=proto.version,
remote_version=remote_services[proto.name])
self.report_error('wrong version')
자 이렇게 전반적인 devp2p 파이썬 구현체를 살펴보았다. 더 궁금한 점은 직접 소스를 살펴보길 바라며, (Framing 부분도 매우 흥미진진하다는 팁을 귀뜸해드린다.) 다음 글에는 [go,rust,c++에서 구현된 devp2p], [devp2p 를 이용하는 blocksync ],[SWARM vs libp2p 를 이용한 IPFS] 에 대해 말해볼 예정이다.
| [하이퍼레저 패브릭 vs CORDA] 조직 구조 비교 (0) | 2018.08.11 |
|---|---|
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
| [블록체인] DApp 플랫폼간 장,단점 (3) | 2018.06.12 |
| [이더리움 메모] 트랜잭션의 실전적 종류 구분. (0) | 2018.06.05 |
| [이더리움 메모] 스마트 컨트랙트와 비용 (0) | 2018.06.01 |
개인적으로 시장을 장악할 DApp 플레이어로 보고 있는 4가지 플랫폼(이더리움,LOOM,하이퍼레저,코다) + 알파에 대해서 정리 해 보려 합니다.
[작성중.......... 틀린 부분도 있을 것이며 당분간은 계속 수정,보안될듯 합니다. ]
특징: 가장 지배적인 & 진정한 범용 퍼블릭 체인/ 스웜,플라즈마,위스퍼,캐스퍼등 꾸준한 플랫폼의 발전 / 이더리움입장에서 EOS 는 퍼블릭 블록체인이 아니다. / 수수로가 많으며, 15tps 정도의 저속이다
- 블록체인 대표 주자!! 안정적이다. (비트코인은 주춧돌, 뒷방 돈 많은 보수적 할아버지) |
|
| (loom network) | 특징: 이더리움의 사이드체인/ERC20 과 호환 토큰/자체 체인을 갖음/ 하드포크를 통한 스마트컨트랙트 업데이트 가능/ FEE 무료/ 고속TPS/ 텔레그램을 통한 한국 개발자와의 적극적인 의사소통. 사용자가 스스로 서브체인 인프라를 만들수도 있고, loom사에서 제공되는 좀비체인도 을 이용할 수도 있다. 스마트컨트랙트 : 솔리디티 - 게임,소셜,엔터테이먼트 등등 |
| Fabric | 특징: 가장 지배적인 범용 엔터프라이즈블록체인/거의 표준/B2B 시스템이나 국가주도 시스템을 장악 할듯/비교적 고속TPS/권한인증시스템/신원확인,개인정보보호/IBM 및 여러 콘소시엄사들의 지원/플러그인 컨센서스/채널을 통한 멀티블록체인 가능(상호교류는 안됨)
- 공공,제조,무역,유통,기록물보관 등등 |
특징: 가장 지배적인 금용권 엔터프라이즈블록체인 중 하나/아키텍트상 금융권에서 사용하기 좋음/ 고속TPS/권한인증시스템/신원확인,개인정보보호/
- 금융 |
|
특징: 이더리움을 가장 위협하는 범용 퍼블릭 체인/고속TPS/FEE 무료
- 게임,소셜,,엔터테이먼트 등등 |
|
| COSMOS, NEO, 오미세고 |
- STEEM SMT 소셜부분에 특화된 블록체인인데, SMT 가 나오면 더 좋아지긴 하겠는데, 모든것을 다 할 수 있는 (?) EOS의 메인넷이 출범하면서 굳이 약소한 형제(?) 인 STEEM 으로 해야하나 싶기도 하다. - 오미세고 OMG |
부록) 블록체인 플랫폼 간 데이터 저장 비용
| Size | RecordsKeeper | Bitcoin | Ethereum | Neo | EOS |
| 1 KiloByte | $ 0.00894 USD | $ 3.835 USD | $ 3.86 USD | $ 19.38 USD | $3 USD |
| 1 MegaByte | $ 8.94 USD | $ 3866.86 USD | $ 3954.49 USD | $ 19742.72 USD | $ 3072 USD |
* Based on USD price on 15 April, 2018 (참고: recordskeeper )
| [이더리움 코어] DevP2P 소스코드 분석 (feat. golang) (0) | 2018.07.11 |
|---|---|
| [이더리움 코어] DevP2P 소스코드 분석 (feat. Python) (0) | 2018.06.27 |
| [이더리움 메모] 트랜잭션의 실전적 종류 구분. (0) | 2018.06.05 |
| [이더리움 메모] 스마트 컨트랙트와 비용 (0) | 2018.06.01 |
| [Ethereum] Node Discovery with Kademlia (1) | 2018.05.18 |
이더리움에서는 각종 함수콜에 관련된 용어와 용도가 난무하여 혼동을 가중 시키고 있는데요. 일반적으로 트랜잭션은 쓰기를 말하고, 콜은 읽기를 말합니다. (CQRS처럼 커맨드와 쿼리가 더 일반적이죠) 하지만 이것은 너무 대략적으로 말한것이라 좀 더 구체적으로 구분해 볼 필요가 생겨서 본 글을 쓰게 되었습니다. (오류가 있을 수 있으며, 이더리움이 업데이트 될 수 있습니다. 재진입같은 보안 이슈는 다루지 않습니다.)
호출 방식
1. Message (Internal tranaction) : 컨트랙트에서 컨트랙트로 호출 하는 것 or 컨트랙트에서 EOA 로 이더전송. 채굴 할 때 까지 대기하지 않으며 즉시 호출. 처음 발생되는 Transaction 호출에 의해 일어나는 부분이다. 따라서 message 에 소모되는 가스 비용은 처음 tranaction 에 포함된다. 최대 1024 Depth 까지 호출될 수 있다.
1-1. 컨트랙트 -> 컨트랙트에는 로우레벨적으로 (메세지) Call 과 (메세지) DelegateCall 이 있다.
Delegatecall은 호출하는 컨트렉트의 context에서 타켓 주소의 코드가 실행되고 msg.sender와 msg.value의 값이 변경되지 않는다는 점을 제외하면 그냥 메시지 콜과 동일하다. (사실 큰 차이이다) 이는 컨트렉트가 실행될 때 다른 주소로부터 코드를 다이내믹하게 읽어들일수 있다는 것을 의미한다. 저장 장소, 현재 주소, 잔액은 여전히 호출하는 주소를 참조하고, 코드만이 호출된 주소로부터 읽어 들이게 된다. msg.sender / tx.origin 는 최초 호출한 EOA 가 되며, this 는 호출한 컨트랙트가 된다. 이는 library함수를 호출할 때와 동일한데 library 를 그냥 콜할때 내부적으로 delegateCall이 호출되기 때문이다. (간단하게 말하면 일반 컨트랜트간 일반 CALL은 저장소 니꺼 사용, DelegateCall은 내꺼 사용)
1-2. 컨트랙트에서 이더 전송 에는 SEND/TRANSFER/CALL 이 있다. 모두 OPCODE 로는 CALL 이다. (
send is now deprecated.)
- (opcode) call 함수가 value와 함께 호출될 때 call함수는 받은 모든 가스를 전달한다. 즉 send/transfer 는 2300가스 고정이고, call 을 이용하면 set(gaslimit) 할수 있다.
- (opcode) call 을 직접적으로 사용해서 컨트랙트를 호출하지 않는것을 권장한다. 그냥 함수이름으로 호출 ㄱㄱ
2. Transaction: EOA 에서 호출하는 것
2-1. sendTransaction: 네트워크에 보내지며 채굴에 의해 확인 받아야 한다. 서명이 필요하며, 채굴 받될 때까지 결과를 얻을 수 없다. 가스를 소모하며 보통 상태를 변경하다. ( 새로운 컨트랙트 만들기, 컨트렉트 호출, 다른 EOA 로 이더보내기)
sendTransaction vs sendRawTransaction
sendTransaction 는 매개변수로 from Address가 필요한데, 트랜잭션에 필요한 개인키를 그걸 통해서 찾기 때문이다. Ethereum client에서 관리하는 개인키를 찾아서 사인해 주세요의 의미이다.
sendRawTransaction 는 from Address가 필요없는데, 개인키 관리를 클라이언트가 알아서 하기 때문이다. 지갑같은 것을 통해 자기가 직접 가져와서 트랜잭션을 직접 서명함. 서명된 데이터(그 안의 private key) 로 부터 public key가 추출되고 그것으로 부터 from Address도 추출되기 때문에 굳이 매개변수로 넣어주지 않는 것이다.
2-2. call: 자신의 노드에서만 실행된다. read-only로 실행되며 즉각적으로 결과를 알 수 있으며, 비용이 들지 않는다.
* yellow paper 에 나오는 "메세지 콜" 이라는 용어는 혼동을 초래하므로 제외 하는게 좋다. 위에 말했다시피, call은 컨트랙트간 호출에서도 사용되는 용어이며, 컨트랙트에서 EOA로 이더를 전송하는 OPCODE에도 CALL이 있다.
아래 3가지 web3 호출 의 차이점
1.testInstance.testfunc({from:eth.accounts[0]})
2.testInstance.testfunc.sendTransaction({from:eth.accounts[0]})
3.testInstance.testfunc.call({from:eth.accounts[0]})
testfunc 가 constant 라벨이 붙었으면, #3번과 같고, 그렇지 않다면 #2와 같다.
정리
* OP 코드에 따라 아래와 같은 가스 소모가든다.
| Operation Name | Gas Cost | Remark |
|---|---|---|
| step | 1 | default amount per an execution cycle |
| stop | 0 | free |
| suicide | 0 | free |
| sha3 | 20 | |
| sload | 20 | get from permanent storage |
| sstore | 100 | put into permanent storage |
| balance | 20 | |
| create | 100 | contract creation |
| call | 20 | initiating a read only call |
| memory | 1 | every additional word when expanding memory |
| txdata | 5 | every byte of data or code for a transaction |
| transaction | 500 | base fee transaction |
| contract creation | 53000 |
changed in homestead from 21000
|
| [이더리움 코어] DevP2P 소스코드 분석 (feat. Python) (0) | 2018.06.27 |
|---|---|
| [블록체인] DApp 플랫폼간 장,단점 (3) | 2018.06.12 |
| [이더리움 메모] 스마트 컨트랙트와 비용 (0) | 2018.06.01 |
| [Ethereum] Node Discovery with Kademlia (1) | 2018.05.18 |
| [블록체인] TPS 그리고 Disruptor 패턴 (0) | 2018.05.12 |
- 앞으로 이더리움 자체에서 코딩할 일이 대부분의 블록체인 개발자의 경우 없어 질 거라 본다. (이유:사이드체인)
- 사이드체인은 비용 걱정할 일이 별로 없다. 메인체인과 인터렉션 할 중요한 경우만~~
- 컨트랙트를 올리는 비용은 크다. (코드 저장비용이 들어가니깐) 컨트랙트를 호출 하는 비용에 비해~
- 데이터 저장에 들어가는 비용은 계산에 들어가는 비용보다 매우 크다.
- 컨트랙트 실행시 가스가 모자르면 그 동안 사용한 가스는 모두 몰수하고 원상복귀다. (예외 발생)
- 송금을 포함한 모든 트랙잭션은 기본적으로 21000가스가 소모되며, 컨트랙트 호출은 +알파(코드실행)가 든다.
- view 키워드가 붙으면 외부에서 호출하는 경우 비용이 들지 않는다. 자기 로컬에서 만 호출하니깐~ (트랜잭션이 아님)
- view 키워드가 붙었지만 내부(동일한 컨트랙) 에서 호출하는 경우, view 성질은 없어지며, 계산 비용이 들어간다.
- 컨트랙트간 호출에서도 상대 컨트랙트의 view 함수를 호출하면 비용이 들지 않겠지.
- 컨트랙트간 호출(메세지콜) 에 들어가는 비용은, 애초에 EOA 에서 호출 할 때 계산되어져야 한다.
- call 은 내부에서 놀고 (비용이 안든다) , 트랜잭션은 외부에서 논다. (기본 비용 등등 든다)
- memory 키워드를 사용하여 비용을 절약하라. 함수외부는 디폴트가 storage 이고, 내부는 memory 지만, 구조체와 배열을 처리하는 경우라면 내부라도 명시적으로 써야 한다. (단순한 복사는 memory, 참조 해서 변경하려면 storage)
- 온갖 mapping 구조를 만들어서 저장공간을 낭비하지말고, 그냥 기본 구조의 loop 돌려서 새로 만들어서 리턴하라.예를들어 (id, kind) (id, count) (kind,count) 등 많이 만들어 두면 get 호출시 속도가 빨라져 좋았었잖아? 근데 블록체인에서는 가격이 더 문제다. 온갖 구조에서 변경이 일어 날때 쓰기 비용은 어마어마 할 것이다.
- address.send 나 address.transfer 의 이더전송 호출에 들어가는 가스는 2300이다. address.call.value(amount)( ) 를 통한 이더전송 호출은 가스를 세팅 할 수 있다.
- 새로 변수 할당 할 때 20000가스, 변수의 값 바꾸는데 5000가스
- 1mb 데이터를 쓴다고 할 때 대략 word 기준 (32byte) 32,768 번 쓰니깐 5 * (20,000 * 32,768) / 1,000,000 ETH, or ~ 3.7628 ETH. 이더당 500~600달러라고 하면, 대략 한국돈으로 150~2백만원 정도 하겠다. 1기가면 몇십억이다.
-
| [블록체인] DApp 플랫폼간 장,단점 (3) | 2018.06.12 |
|---|---|
| [이더리움 메모] 트랜잭션의 실전적 종류 구분. (0) | 2018.06.05 |
| [Ethereum] Node Discovery with Kademlia (1) | 2018.05.18 |
| [블록체인] TPS 그리고 Disruptor 패턴 (0) | 2018.05.12 |
| [이더리움] Merkle Patricia Tree (MPT) 를 이해하기 위한 여정 (4) | 2018.05.10 |