From 6ac6397d33801c6c4dbdfd38a8d34e781ccedf15 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Mon, 9 Jul 2018 20:00:34 +0300 Subject: [PATCH] Added documentation --- README.md | 230 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 188 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6210798..04b9c9d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,85 @@ -**nim-eth-p2p** [![Build Status](https://travis-ci.org/status-im/nim-eth-p2p.svg?branch=master)](https://travis-ci.org/status-im/nim-eth-p2p) [![Build status](https://ci.appveyor.com/api/projects/status/i4txsa2pdyaahmn0/branch/master?svg=true)](https://ci.appveyor.com/project/cheatfate/nim-eth-p2p/branch/master)[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)![Stability: experimental](https://img.shields.io/badge/stability-experimental-orange.svg) +**nim-eth-p2p** +[![Build Status](https://travis-ci.org/status-im/nim-eth-p2p.svg?branch=master)](https://travis-ci.org/status-im/nim-eth-p2p) +[![Build status](https://ci.appveyor.com/api/projects/status/i4txsa2pdyaahmn0/branch/master?svg=true)](https://ci.appveyor.com/project/cheatfate/nim-eth-p2p/branch/master) +[![License: Apache](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Stability: experimental](https://img.shields.io/badge/stability-experimental-orange.svg)] ## Introduction -This library is a Nim re-implementation of the Ethereum DevP2P networking protocol. +This library implements the DevP2P family of networking protocols used +in the Ethereum world. ## Installation -``` -$ nimble install eth_p2p +``` bash +nimble install eth_p2p ``` -## RLPx +## Connecting to the Ethereum network + +A connection to the Ethereum network can be created by instantiating +the `EthereumNode` type: + +``` nim +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](https://github.com/status-im/nim-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](https://github.com/status-im/nim-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: + + ```nim + 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`: + +``` nim +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](https://github.com/ethereum/devp2p/blob/master/rlpx.md) is the high-level protocol for exchanging messages between peers in the Ethereum @@ -18,14 +87,17 @@ 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. -To obtain a RLPx connection, use the `rlpxConnect` proc supplying the -id of another node in the network. On success, the proc will return a -`Peer` object representing the connection. Each of the RLPx sub-protocols -consists of a set of strongly-typed messages, which are represented by -this library as regular Nim procs that can be executed over the `Peer` -object (more on this later). +The RLPx protocols are defined as a collection of strongly-typed messages, +which are grouped into sub-protocols multiplexed over the same TCP connection. -### Defining RLPx sub-protocols +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: @@ -41,36 +113,26 @@ rlpxProtocol p2p, 0: listenPort: uint, nodeId: P2PNodeId) = peer.id = nodeId - peer.dispatcher = getDispatcher(capabilities) proc disconnect(peer: Peer, reason: DisconnectionReason) - proc ping(peer: Peer) + proc ping(peer: Peer) = + await peer.pong() proc pong(peer: Peer) = echo "received pong from ", peer.id ``` -#### Sending messages +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. -To send a particular message to a particular peer, just call the -corresponding proc over the `Peer` object: +### Protocol state -``` nim -peer.hello(4, "Nimbus 1.0", ...) -peer.ping() -``` - -#### Receiving messages - -Once a connection is established, incoming messages in RLPx may appear in -arbitrary order, because the sub-protocols may be multiplexed over a single -underlying connection. For this reason, the library assumes that the incoming -messages will be dispatched automatically to their corresponding handlers, -appearing in the protocol definition. 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: +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: ``` nim rlpxProtocol abc, 1: @@ -82,18 +144,40 @@ rlpxProtocol abc, 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: + +```nim +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` accessor: +specify the protocol identifier to the `state` accessors: ``` nim echo "ABC protocol messages: ", peer.state(abc).receivedMsgCount ``` -While the state machine approach is the recommended way of implementing -sub-protocols, sometimes in imperative code it may be easier to wait for -a particular response message after sending a certain request. +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: -This is enabled by the helper proc `nextMsg`: +### 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: ``` nim proc handshakeExample(peer: Peer) {.async.} = @@ -117,11 +201,73 @@ There are few things to note in the above example: matching the parameter names of the message. If the messages has `openarray` params, these will be remapped to `seq` types. -The future returned by `nextMsg` will be resolved only after the handler of the -designated message 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. +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 + +``` nim +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: + +``` nim +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](https://github.com/zsfelfoldi/go-ethereum/wiki/Light-Ethereum-Subprotocol-%28LES%29), +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: + +``` nim +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