하마
5k
2018-07-12 18:35:50 작성 2018-07-15 20:49:45 수정됨
0
748

[이더리움 코어] DevP2P 소스코드 분석 (go 언어 기반)




아직 블록체인 관련 기술이 백서를 넘어서 활

성화 되는 단계가 아니라서 (가능성을 알아보

는 프로토 타입의 시대. 그리고 중앙화된 거래

소만 엄청난 수익을 얻는 아이러니한 시대) 내

용이 좀 재미가 없긴 할거 같긴 하네요. 


고(go)언어 스터디하는 분들이라면 소스 읽어

보면  많이 늘거 같습니다.@@

참고로 파이썬 스터디 하는 분들이라면 이 연

재의 이전 편이 파이썬으로 되어 있으니 참고 

하시구요 ^^


* 본격적인 블록체인 시대가 열리면 , 블록체

인 분야에서는 웹풀스택개발자를 더 많이

필요로 할 것입니다. 현재 모든 분야에서 풀스

택개발자는 필수니까요~ 여담으로 컴공졸업

하고 뭘 해야 할지 모르겠다는 조카에게도 자

바스크립트 기반 풀스택에 집중해보는게

어떻겠냐고 조언을 해줬습니다. 사람이 없

죠.. 


서론

이전 글에서는 파이썬 기반으로 분석해 보았


는데 이번에는 go-ethereum의 devp2p 를 대


상으로 한다. 

이름이 나타내는 것처럼 go-ethereum은 구


글에서 만든 go언어 기반인데, 




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

FM대로 생활하는/모든것에 주기표가 붙어 있는 군인의 삶의 행적을 쫒는게 쉬울까? 자유롭게 여행다니는 사람의 행적을 쫒는게 쉬울까?
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 의 경우 전체적으로 조망하는 능력이 더 필요하다고 볼 수 있다. 위에서 switchd 처럼 생긴 select 문은 이벤트가 발생하길 기다리는 녀석이다. 이 처럼 go-ethereum의 코드는 생산자(이벤트발생)-소비자(이벤트소비)가 무지하게 복잡하게 얽혀있는 구조를 가진다. (그나마 다행인것은 언어 자체적으로 지원하는 chan 이라는 키워드 덕분에 순서 관계가 명확해 진다는 것이다. 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에 대해서 대략 살펴봤기 때문에 이번 글에서는 주로 응용프로토콜과의 인터페이싱 부분(위 그림의 노란 화살표)에 대해서 살펴 볼 것이다.

2.  Rlpx - 살펴보기

이번에는 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(...) 을 통해서 본격적인 핸드쉐이킹이 시작된다.

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}
          ...

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

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/hander.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
  }
  
   ....
}

위에서 프로토콜 길이를 보면 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)

// handleMsg is invoked whenever an inbound message is received from a remote
// peer. The remote connection is torn down upon returning any error. 
func (pm *ProtocolManager) handleMsg(p *peer) error {

msg, err := p.rw.ReadMsg() // p2p 코어를 통해서 데이터를 읽은 후에 

// 읽은 데이터에 대한 핸들링 을 해줍니다. 구체적인 로직은 생략 (이 부분은 다음 연재글을 통해서 알아보죠) 
switch {
case msg.Code == StatusMsg:
...
case msg.Code == GetBlockHeadersMsg:
...

case msg.Code == BlockHeadersMsg:
...

case msg.Code == GetBlockBodiesMsg:
...

case msg.Code == BlockBodiesMsg:
...

case p.version >= eth63 && msg.Code == GetNodeDataMsg:
...

case p.version >= eth63 && msg.Code == NodeDataMsg:
...

case p.version >= eth63 && msg.Code == ReceiptsMsg:
...

case msg.Code == NewBlockHashesMsg:
...

case msg.Code == NewBlockMsg:
...

case msg.Code == TxMsg:
...

default:
return errResp(ErrInvalidMsgCode, "%v", msg.Code)
}
return nil
}

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

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


1
1
  • 댓글 0

  • 로그인을 하시면 댓글을 등록할 수 있습니다.