Nim Ethereum P2P protocol implementation
Go to file
Jacek Sieka 0909540a92
ci: update
readme: fix license, badges
2018-09-04 22:24:14 -06:00
eth_p2p Download block bodies 2018-08-29 23:34:21 +03:00
tests More fixes. Block headers download works! 2018-08-14 16:45:08 +03:00
.gitignore progress on implementing RLPx 2018-04-01 05:41:05 +03:00
.travis.yml ci: update 2018-09-04 22:24:14 -06:00
LICENSE-APACHEv2 WIP refactor the rlpxProtocol macro 2018-07-06 13:24:01 +03:00
LICENSE-MIT WIP refactor the rlpxProtocol macro 2018-07-06 13:24:01 +03:00
README.md ci: update 2018-09-04 22:24:14 -06:00
appveyor.yml ci: update 2018-09-04 22:24:14 -06:00
eth_p2p.nim Compatibility with newer nim 2018-08-29 18:28:01 +03:00
eth_p2p.nim.cfg Move the code from rlpx.nim in the base module as it now represents 2018-07-12 14:14:22 +03:00
eth_p2p.nimble Temporary workaround to experimental problem 2018-08-29 18:28:01 +03:00

README.md

eth_p2p

Build Status (Travis) Windows build status (Appveyor) License: Apache License: MIT Stability: experimental

Introduction

This library implements the DevP2P family of networking protocols used in the Ethereum world.

Installation

nimble install eth_p2p

Connecting to the Ethereum network

A connection to the Ethereum network can be created by instantiating the EthereumNode type:

proc newEthereumNode*(keys: KeyPair,
                      chain: AbstractChainDB,
                      clientId = "nim-eth-p2p",
                      addAllCapabilities = true): EthereumNode =

Parameters:

keys: A pair of public and private keys used to authenticate the node on the network and to determine its node ID. See the eth_keys library for utilities that will help you generate and manage such keys.

chain: An abstract instance of the Ethereum blockchain associated with the node. This library allows you to plug any instance conforming to the abstract interface defined in the eth_common package.

clientId: A name used to identify the software package connecting to the network (i.e. similar to the User-Agent string in a browser).

addAllCapabilities: By default, the node will support all RPLx protocols imported in your project. You can specify false if you prefer to create a node with a more limited set of protocols. Use one or more calls to node.addCapability to specify the desired set:

node.addCapability(eth)
node.addCapability(ssh)

Each supplied protocol identifier is a name of a protocol introduced by the rlpxProtocol macro discussed later in this document.

Instantiating an EthereumNode does not immediately connect you to the network. To start the connection process, call node.connectToNetwork:

proc connectToNetwork*(node: var EthereumNode,
                       address: Address,
                       listeningPort = Port(30303),
                       bootstrapNodes: openarray[ENode],
                       networkId: int,
                       startListening = true)

The EthereumNode will automatically find and maintan a pool of peers using the Ethereum node discovery protocol. You can access the pool as node.peers.

Communicating with Peers using RLPx

RLPx is the high-level protocol for exchanging messages between peers in the Ethereum network. Most of the client code of this library should not be concerned with the implementation details of the underlying protocols and should use the high-level APIs described in this section.

The RLPx protocols are defined as a collection of strongly-typed messages, which are grouped into sub-protocols multiplexed over the same TCP connection.

This library represents each such message as a regular Nim function call over the Peer object. Certain messages act only as notifications, while others fit the request/response pattern.

To understand more about how messages are defined and used, let's look at the definition of a RLPx protocol:

RLPx sub-protocols

The sub-protocols are defined with the rlpxProtocol macro. It will accept a 3-letter identifier for the protocol and the current protocol version:

Here is how the DevP2P wire protocol might look like:

rlpxProtocol p2p, 0:
  proc hello(peer: Peer,
             version: uint,
             clientId: string,
             capabilities: openarray[Capability],
             listenPort: uint,
             nodeId: P2PNodeId) =
    peer.id = nodeId

  proc disconnect(peer: Peer, reason: DisconnectionReason)

  proc ping(peer: Peer) =
    await peer.pong()

  proc pong(peer: Peer) =
    echo "received pong from ", peer.id

As seen in the example above, a protocol definition determines both the available messages that can be sent to another peer (e.g. as in peer.pong()) and the asynchronous code responsible for handling the incoming messages.

Protocol state

The protocol implementations are expected to maintain a state and to act like a state machine handling the incoming messages. To achieve this, each protocol may define a State object that can be accessed as a state field of the Peer object:

rlpxProtocol abc, 1:
  type State = object
    receivedMsgsCount: int

  proc incomingMessage(p: Peer) =
    p.state.receivedMsgsCount += 1

Besides the per-peer state demonstrated above, there is also support for maintaining a network-wide state. In the example above, we'll just have to change the name of the state type to NetworkState and the accessor expression to p.network.state.

The state objects are initialized to zero by default, but you can modify this behaviour by overriding the following procs for your state types:

proc initProtocolState*(state: var MyPeerState, p: Peer)
proc initProtocolState*(state: var MyNetworkState, n: EthereumNode)

Please note that the state type will have to be placed outside of the protocol definition in order to achieve this.

Sometimes, you'll need to access the state of another protocol. To do this, specify the protocol identifier to the state accessors:

  echo "ABC protocol messages: ", peer.state(abc).receivedMsgCount

While the state machine approach may be a particularly robust way of implementing sub-protocols (it is more amenable to proving the correctness of the implementation through formal verification methods), sometimes it may be more convenient to use more imperative style of communication where the code is able to wait for a particular response after sending a particular request. The library provides two mechanisms for achieving this:

Waiting particular messages with nextMsg

The nextMsg helper proc can be used to pause the execution of an async proc until a particular incoming message from a peer arrives:

proc handshakeExample(peer: Peer) {.async.} =
  ...
  # send a hello message
  peer.hello(...)

  # wait for a matching hello response
  let response = await peer.nextMsg(p2p.hello)
  echo response.clientId # print the name of the Ethereum client
                         # used by the other peer (Geth, Parity, Nimbus, etc)

There are few things to note in the above example:

  1. The rlpxProtocol definition created a pseudo-variable named after the protocol holding various properties of the protocol.

  2. Each message defined in the protocol received a corresponding type name, matching the message name (e.g. p2p.hello). This type will have fields matching the parameter names of the message. If the messages has openarray params, these will be remapped to seq types.

If the designated messages also has an attached handler, the future returned by nextMsg will be resolved only after the handler has been fully executed (so you can count on any side effects produced by the handler to have taken place). If there are multiple outstanding calls to nextMsg, they will complete together. Any other messages received in the meantime will still be dispatched to their respective handlers.

requestResponse pairs

rlpxProtocol les, 2:
  ...

  requestResponse:
    proc getProofs(p: Peer, proofs: openarray[ProofRequest])
    proc proofs(p: Peer, BV: uint, proofs: openarray[Blob])
  
  ...

Two or more messages within the protocol may be grouped into a requestResponse block. The last message in the group is assumed to be the response while all other messages are considered requests.

When a request message is sent, the return type will be a Future that will be completed once the response is received. Please note that there is a mandatory timeout parameter, so the actual return type is Future[Option[MessageType]]. The timeout parameter can be specified for each individual call and the default value can be overridden on the level of individual message, or the entire protocol:

rlpxProtocol abc, 1:
  timeout = 5000 # value in milliseconds
  useRequestIds = false
  
  requestResponse:
    proc myReq(dataId: int, timeout = 3000)
    proc myRes(data: string)

By default, the library will take care of inserting a hidden reqId parameter as used in the LES protocol, but you can disable this behavior by overriding the protocol setting useRequestIds.

Implementing handshakes and reacting to other events

Besides message definitions and implementations, a protocol specification may also include handlers for certain important events such as newly connected peers or misbehaving or disconnecting peers:

rlpxProtocol les, 2:
  onPeerConnected do (peer: Peer):
    asyncCheck peer.status [
      "networkId": rlp.encode(1),
      "keyGenesisHash": rlp.encode(peer.network.chain.genesisHash)
      ...
    ]

    let otherPeerStatus = await peer.nextMsg(les.status)
    ...

  onPeerDisconnected do (peer: Peer, reason: DisconnectionReason):
    debug "peer disconnected", peer

Checking the other peer's supported sub-protocols

Upon establishing a connection, RLPx will automatically negotiate the list of mutually supported protocols by the peers. To check whether a particular peer supports a particular sub-protocol, use the following code:

if peer.supports(les): # `les` is the identifier of the light clients sub-protocol
  peer.getReceipts(nextReqId(), neededReceipts())

License

Licensed under both of the following: