Document the RLPx public APIs
This commit is contained in:
parent
6ee66c3d00
commit
8c79997672
125
README.md
125
README.md
|
@ -1,6 +1,129 @@
|
||||||
# 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)
|
# 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)
|
||||||
|
|
||||||
Nim Ethereum P2P protocol implementation
|
[[Nim]] Ethereum P2P protocol implementation
|
||||||
|
|
||||||
|
## RLPx
|
||||||
|
|
||||||
|
[RLPx](https://github.com/ethereum/devp2p/blob/master/rlpx.md) 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.
|
||||||
|
|
||||||
|
To obtain a RLPx connection, use the proc `rlpxConnect` 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).
|
||||||
|
|
||||||
|
### Defining 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](https://github.com/ethereum/wiki/wiki/%C3%90%CE%9EVp2p-Wire-Protocol) might look like:
|
||||||
|
|
||||||
|
``` nim
|
||||||
|
rlpxProtocol p2p, 0:
|
||||||
|
proc hello(peer: Peer,
|
||||||
|
version: uint,
|
||||||
|
clientId: string,
|
||||||
|
capabilities: openarray[Capability],
|
||||||
|
listenPort: uint,
|
||||||
|
nodeId: P2PNodeId) =
|
||||||
|
peer.id = nodeId
|
||||||
|
peer.dispatcher = getDispatcher(capabilities)
|
||||||
|
|
||||||
|
proc disconnect(peer: Peer, reason: DisconnectionReason)
|
||||||
|
|
||||||
|
proc ping(peer: Peer)
|
||||||
|
|
||||||
|
proc pong(peer: Peer) =
|
||||||
|
echo "received pong from ", peer.id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sending messages
|
||||||
|
|
||||||
|
To send a particular message to a particular peer, just call the
|
||||||
|
corresponding proc over the `Peer` object:
|
||||||
|
|
||||||
|
``` 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:
|
||||||
|
|
||||||
|
``` nim
|
||||||
|
rlpxProtocol abc, 1:
|
||||||
|
type State = object
|
||||||
|
receivedMsgsCount: int
|
||||||
|
|
||||||
|
proc incomingMessage(p: Peer) =
|
||||||
|
p.state.receivedMsgsCount += 1
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes, you'll need to access the state of another protocol. To do this,
|
||||||
|
specify the protocol identifier to the `state` accessor:
|
||||||
|
|
||||||
|
``` 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.
|
||||||
|
|
||||||
|
This is enabled by the helper proc `nextMsg`:
|
||||||
|
|
||||||
|
``` nim
|
||||||
|
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.
|
||||||
|
|
||||||
|
By default, `nextMsg` will still automatically dispatch all messages different
|
||||||
|
from the awaited one, but you can prevent this behavior by specifying the extra
|
||||||
|
flag `discardOthers = true`.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
``` nim
|
||||||
|
if peer.supports(les): # `les` is the identifier of the light clients sub-protocol
|
||||||
|
peer.getReceipts(nextReqId(), neededReceipts())
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
281
ethp2p/rlpx.nim
281
ethp2p/rlpx.nim
|
@ -1,19 +1,30 @@
|
||||||
import
|
import
|
||||||
macros, sets, algorithm, async, asyncnet, hashes, rlp, ecc,
|
macros, sets, algorithm, async, asyncnet, asyncfutures,
|
||||||
ethereum_types, kademlia, discovery, auth
|
hashes, rlp, ranges/ptr_arith, eth_keys, ethereum_types,
|
||||||
|
kademlia, discovery, auth
|
||||||
|
|
||||||
type
|
type
|
||||||
|
P2PNodeId = MDigest[512]
|
||||||
|
|
||||||
|
ConnectionState = enum
|
||||||
|
None,
|
||||||
|
Connected,
|
||||||
|
Disconnecting,
|
||||||
|
Disconnected
|
||||||
|
|
||||||
Peer* = ref object
|
Peer* = ref object
|
||||||
id: NodeId # XXX: not fillet yed
|
id: P2PNodeId # XXX: not fillet yed
|
||||||
socket: AsyncSocket
|
socket: AsyncSocket
|
||||||
dispatcher: Dispatcher
|
dispatcher: Dispatcher
|
||||||
# privKey: AesKey
|
# privKey: AesKey
|
||||||
networkId: int
|
networkId: int
|
||||||
sessionSecrets: ConnectionSecret
|
sessionSecrets: ConnectionSecret
|
||||||
|
connectionState: ConnectionState
|
||||||
|
protocolStates: seq[RootRef]
|
||||||
|
|
||||||
MessageHandler* = proc(x: Peer, data: var Rlp)
|
MessageHandler* = proc(x: Peer, data: var Rlp)
|
||||||
|
|
||||||
MessageDesc* = object
|
MessageInfo* = object
|
||||||
id*: int
|
id*: int
|
||||||
name*: string
|
name*: string
|
||||||
thunk*: MessageHandler
|
thunk*: MessageHandler
|
||||||
|
@ -24,10 +35,10 @@ type
|
||||||
name*: CapabilityName
|
name*: CapabilityName
|
||||||
version*: int
|
version*: int
|
||||||
|
|
||||||
Protocol* = ref object
|
ProtocolInfo* = ref object
|
||||||
name*: CapabilityName
|
name*: CapabilityName
|
||||||
version*: int
|
version*: int
|
||||||
messages*: seq[MessageDesc]
|
messages*: seq[MessageInfo]
|
||||||
index: int # the position of the protocol in the
|
index: int # the position of the protocol in the
|
||||||
# ordered list of supported protocols
|
# ordered list of supported protocols
|
||||||
|
|
||||||
|
@ -64,9 +75,10 @@ const
|
||||||
maxUInt24 = (not uint32(0)) shl 8
|
maxUInt24 = (not uint32(0)) shl 8
|
||||||
|
|
||||||
var
|
var
|
||||||
gProtocols = newSeq[Protocol](0)
|
gProtocols = newSeq[ProtocolInfo](0)
|
||||||
|
gCapabilities = newSeq[Capability](0)
|
||||||
gDispatchers = initSet[Dispatcher]()
|
gDispatchers = initSet[Dispatcher]()
|
||||||
devp2p: Protocol
|
devp2p: ProtocolInfo
|
||||||
|
|
||||||
# Dispatcher
|
# Dispatcher
|
||||||
#
|
#
|
||||||
|
@ -90,7 +102,7 @@ proc describeProtocols(d: Dispatcher): string =
|
||||||
if result.len != 0: result.add(',')
|
if result.len != 0: result.add(',')
|
||||||
for c in gProtocols[i].name: result.add(c)
|
for c in gProtocols[i].name: result.add(c)
|
||||||
|
|
||||||
proc getDispatcher(otherPeerCapabilities: var openarray[Capability]): Dispatcher =
|
proc getDispatcher(otherPeerCapabilities: openarray[Capability]): Dispatcher =
|
||||||
# XXX: sub-optimal solution until progress is made here:
|
# XXX: sub-optimal solution until progress is made here:
|
||||||
# https://github.com/nim-lang/Nim/issues/7457
|
# https://github.com/nim-lang/Nim/issues/7457
|
||||||
# We should be able to find an existing dispatcher without allocating a new one
|
# We should be able to find an existing dispatcher without allocating a new one
|
||||||
|
@ -130,10 +142,10 @@ proc getDispatcher(otherPeerCapabilities: var openarray[Capability]): Dispatcher
|
||||||
|
|
||||||
gDispatchers.incl result
|
gDispatchers.incl result
|
||||||
|
|
||||||
# Protocol
|
# Protocol info objects
|
||||||
#
|
#
|
||||||
|
|
||||||
proc newProtocol(name: string, version: int): Protocol =
|
proc newProtocol(name: string, version: int): ProtocolInfo =
|
||||||
new result
|
new result
|
||||||
result.name[0] = name[0]
|
result.name[0] = name[0]
|
||||||
result.name[1] = name[1]
|
result.name[1] = name[1]
|
||||||
|
@ -141,24 +153,26 @@ proc newProtocol(name: string, version: int): Protocol =
|
||||||
result.version = version
|
result.version = version
|
||||||
result.messages = @[]
|
result.messages = @[]
|
||||||
|
|
||||||
proc nameStr*(p: Protocol): string =
|
proc nameStr*(p: ProtocolInfo): string =
|
||||||
result = newStringOfCap(3)
|
result = newStringOfCap(3)
|
||||||
for c in p.name: result.add(c)
|
for c in p.name: result.add(c)
|
||||||
|
|
||||||
proc cmp*(lhs, rhs: Protocol): int {.inline.} =
|
proc cmp*(lhs, rhs: ProtocolInfo): int {.inline.} =
|
||||||
for i in 0..2:
|
for i in 0..2:
|
||||||
if lhs.name[i] != rhs.name[i]:
|
if lhs.name[i] != rhs.name[i]:
|
||||||
return int16(lhs.name[i]) - int16(rhs.name[i])
|
return int16(lhs.name[i]) - int16(rhs.name[i])
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
proc registerMessage(protocol: var Protocol,
|
proc registerMsg(protocol: var ProtocolInfo,
|
||||||
id: int, name: string, thunk: MessageHandler) =
|
id: int, name: string, thunk: MessageHandler) =
|
||||||
protocol.messages.add MessageDesc(id: id, name: name, thunk: thunk)
|
protocol.messages.add MessageInfo(id: id, name: name, thunk: thunk)
|
||||||
|
|
||||||
proc registerProtocol(protocol: Protocol) =
|
proc registerProtocol(protocol: ProtocolInfo) =
|
||||||
# XXX: This can be done at compile-time in the future
|
# XXX: This can be done at compile-time in the future
|
||||||
if protocol.version > 0:
|
if protocol.version > 0:
|
||||||
gProtocols.insert(protocol, lowerBound(gProtocols, protocol))
|
let pos = lowerBound(gProtocols, protocol)
|
||||||
|
gProtocols.insert(protocol, pos)
|
||||||
|
gCapabilities.insert(Capability(name: protocol.name, version: protocol.version), pos)
|
||||||
for i in 0 ..< gProtocols.len:
|
for i in 0 ..< gProtocols.len:
|
||||||
gProtocols[i].index = i
|
gProtocols[i].index = i
|
||||||
else:
|
else:
|
||||||
|
@ -173,34 +187,37 @@ proc append*(rlpWriter: var RlpWriter, hash: KeccakHash) =
|
||||||
proc read*(rlp: var Rlp, T: typedesc[KeccakHash]): T =
|
proc read*(rlp: var Rlp, T: typedesc[KeccakHash]): T =
|
||||||
result.data = rlp.read(type(result.data))
|
result.data = rlp.read(type(result.data))
|
||||||
|
|
||||||
proc append*(rlpWriter: var RlpWriter, p: Protocol) =
|
|
||||||
append(rlpWriter, (p.nameStr, p.version))
|
|
||||||
|
|
||||||
proc read*(rlp: var Rlp, T: type Protocol): Protocol =
|
|
||||||
let cap = rlp.read(Capability)
|
|
||||||
for p in gProtocols:
|
|
||||||
if p.name == cap.name and p.version == cap.version:
|
|
||||||
return p
|
|
||||||
# XXX: This shouldn't return nil probably, but rather
|
|
||||||
# an empty Protocol object
|
|
||||||
return nil
|
|
||||||
|
|
||||||
# Message composition and encryption
|
# Message composition and encryption
|
||||||
#
|
#
|
||||||
|
|
||||||
proc writeMessageId(p: Protocol, msgId: int, peer: Peer, rlpOut: var RlpWriter) =
|
proc writeMsgId(p: ProtocolInfo, msgId: int, peer: Peer, rlpOut: var RlpWriter) =
|
||||||
let baseMsgId = peer.dispatcher.protocolOffsets[p.index]
|
let baseMsgId = peer.dispatcher.protocolOffsets[p.index]
|
||||||
if baseMsgId == -1:
|
if baseMsgId == -1:
|
||||||
raise newException(UnsupportedProtocol,
|
raise newException(UnsupportedProtocol,
|
||||||
p.nameStr & " is not supported by peer " & $peer.id)
|
p.nameStr & " is not supported by peer " & $peer.id)
|
||||||
rlpOut.append(baseMsgId + msgId)
|
rlpOut.append(baseMsgId + msgId)
|
||||||
|
|
||||||
|
proc dispatchMsg(peer: Peer, msgId: int, msgData: var Rlp) =
|
||||||
|
template invalidIdError: untyped =
|
||||||
|
raise newException(ValueError,
|
||||||
|
"RLPx message with an invalid id " & $msgId &
|
||||||
|
" on a connection supporting " & peer.dispatcher.describeProtocols)
|
||||||
|
|
||||||
|
if msgId >= peer.dispatcher.thunks.len: invalidIdError()
|
||||||
|
let thunk = peer.dispatcher.thunks[msgId]
|
||||||
|
if thunk == nil: invalidIdError()
|
||||||
|
|
||||||
|
thunk(peer, msgData)
|
||||||
|
|
||||||
proc updateMac(mac: var openarray[byte], key: openarray[byte], bytes: openarray[byte]) =
|
proc updateMac(mac: var openarray[byte], key: openarray[byte], bytes: openarray[byte]) =
|
||||||
# XXX TODO: implement this
|
# XXX TODO: implement this
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
type
|
||||||
|
RlpxHeader = array[32, byte]
|
||||||
|
|
||||||
proc send(p: Peer, data: BytesRange) =
|
proc send(p: Peer, data: BytesRange) =
|
||||||
var header: array[32, byte]
|
var header: RlpxHeader
|
||||||
if data.len > int(maxUInt24):
|
if data.len > int(maxUInt24):
|
||||||
raise newException(OverflowError, "RLPx message size exceeds limit")
|
raise newException(OverflowError, "RLPx message size exceeds limit")
|
||||||
|
|
||||||
|
@ -242,24 +259,47 @@ proc send(p: Peer, data: BytesRange) =
|
||||||
return header_ciphertext + header_mac + frame_ciphertext + frame_mac
|
return header_ciphertext + header_mac + frame_ciphertext + frame_mac
|
||||||
"""
|
"""
|
||||||
|
|
||||||
proc dispatchMessage(connection: Peer, msg: BytesRange) =
|
proc getMsgLen(header: RlpxHeader): int =
|
||||||
# This proc dispatches an already decrypted message
|
32
|
||||||
|
|
||||||
var rlp = rlpFromBytes(msg)
|
proc fullRecvInto(s: AsyncSocket, buffer: pointer, bufferLen: int) {.async.} =
|
||||||
|
# XXX: This should be a library function
|
||||||
|
var receivedBytes = 0
|
||||||
|
while receivedBytes < bufferLen:
|
||||||
|
receivedBytes += await s.recvInto(buffer.shift(receivedBytes),
|
||||||
|
bufferLen - receivedBytes)
|
||||||
|
|
||||||
|
proc recvMsg*(peer: Peer): Future[tuple[msgId: int, msgData: Rlp]] {.async.} =
|
||||||
|
## This procs awaits the next complete RLPx message in the TCP stream
|
||||||
|
|
||||||
|
var header: RlpxHeader
|
||||||
|
await peer.socket.fullRecvInto(header.baseAddr, sizeof(header))
|
||||||
|
|
||||||
|
let msgLen = header.getMsgLen
|
||||||
|
var msgData = newSeq[byte](msgLen).toRange
|
||||||
|
await peer.socket.fullRecvInto(msgData.baseAddr, msgData.len)
|
||||||
|
|
||||||
|
var rlp = rlpFromBytes(msgData)
|
||||||
let msgId = rlp.read(int)
|
let msgId = rlp.read(int)
|
||||||
|
return (msgId, rlp)
|
||||||
|
|
||||||
template invalidIdError: untyped =
|
proc nextMsg*(peer: Peer, MsgType: typedesc,
|
||||||
raise newException(ValueError,
|
discardOthers = false): Future[MsgType] {.async.} =
|
||||||
"RLPx message with an invalid id " & $msgId &
|
## This procs awaits a specific RLPx message.
|
||||||
" on a connection supporting " & connection.dispatcher.describeProtocols)
|
## By default, other messages will be automatically dispatched
|
||||||
|
## to their responsive handlers unless `discardOthers` is set to
|
||||||
|
## true. This may be useful when the protocol requires a very
|
||||||
|
## specific response to a given request. Use with caution.
|
||||||
|
const wantedId = MsgType.msgId
|
||||||
|
|
||||||
if msgId >= connection.dispatcher.thunks.len: invalidIdError()
|
while true:
|
||||||
let thunk = connection.dispatcher.thunks[msgId]
|
var (nextMsgId, nextMsgData) = await peer.recvMsg()
|
||||||
if thunk == nil: invalidIdError()
|
if nextMsgId == wantedId:
|
||||||
|
return nextMsgData.read(MsgType)
|
||||||
|
elif not discardOthers:
|
||||||
|
peer.dispatchMsg(nextMsgId, nextMsgData)
|
||||||
|
|
||||||
thunk(connection, rlp)
|
iterator typedParams(n: NimNode, skip = 0): (NimNode, NimNode) =
|
||||||
|
|
||||||
iterator typedParams(n: PNimrodNode, skip = 0): (PNimrodNode, PNimrodNode) =
|
|
||||||
for i in (1 + skip) ..< n.params.len:
|
for i in (1 + skip) ..< n.params.len:
|
||||||
let paramNodes = n.params[i]
|
let paramNodes = n.params[i]
|
||||||
let paramType = paramNodes[^2]
|
let paramType = paramNodes[^2]
|
||||||
|
@ -267,12 +307,36 @@ iterator typedParams(n: PNimrodNode, skip = 0): (PNimrodNode, PNimrodNode) =
|
||||||
for j in 0 .. < (paramNodes.len-2):
|
for j in 0 .. < (paramNodes.len-2):
|
||||||
yield (paramNodes[j], paramType)
|
yield (paramNodes[j], paramType)
|
||||||
|
|
||||||
macro rlpxProtocol*(name: static[string],
|
proc chooseFieldType(n: NimNode): NimNode =
|
||||||
|
## Examines the parameter types used in the message signature
|
||||||
|
## and selects the corresponding field type for use in the
|
||||||
|
## message object type (i.e. `p2p.hello`).
|
||||||
|
##
|
||||||
|
## For now, only openarray types are remapped to sequences.
|
||||||
|
result = n
|
||||||
|
if n.kind == nnkBracketExpr and
|
||||||
|
n[0].kind == nnkIdent and
|
||||||
|
$n[0].ident == "openarray":
|
||||||
|
result = n.copyNimTree
|
||||||
|
result[0] = newIdentNode("seq")
|
||||||
|
|
||||||
|
proc getState(peer: Peer, proto: ProtocolInfo): RootRef =
|
||||||
|
peer.protocolStates[proto.index]
|
||||||
|
|
||||||
|
template state*(connection: Peer, Protocol: typedesc): untyped =
|
||||||
|
## Returns the state object of a particular protocol for a
|
||||||
|
## particular connection.
|
||||||
|
cast[ref Protocol.State](connection.getState(Protocol.info))
|
||||||
|
|
||||||
|
macro rlpxProtocol*(protoIdentifier: untyped,
|
||||||
version: static[int],
|
version: static[int],
|
||||||
body: untyped): untyped =
|
body: untyped): untyped =
|
||||||
|
## The macro used to defined RLPx sub-protocols. See README.
|
||||||
var
|
var
|
||||||
|
protoName = $protoIdentifier
|
||||||
|
protoNameIdent = newIdentNode(protoName)
|
||||||
nextId = BiggestInt 0
|
nextId = BiggestInt 0
|
||||||
protocol = genSym(nskVar)
|
protocol = genSym(nskVar, protoName & "Proto")
|
||||||
newProtocol = bindSym "newProtocol"
|
newProtocol = bindSym "newProtocol"
|
||||||
rlpFromBytes = bindSym "rlpFromBytes"
|
rlpFromBytes = bindSym "rlpFromBytes"
|
||||||
read = bindSym "read"
|
read = bindSym "read"
|
||||||
|
@ -281,12 +345,23 @@ macro rlpxProtocol*(name: static[string],
|
||||||
append = bindSym "append"
|
append = bindSym "append"
|
||||||
send = bindSym "send"
|
send = bindSym "send"
|
||||||
Peer = bindSym "Peer"
|
Peer = bindSym "Peer"
|
||||||
writeMessageId = bindSym "writeMessageId"
|
writeMsgId = bindSym "writeMsgId"
|
||||||
isSubprotocol = version > 0
|
isSubprotocol = version > 0
|
||||||
|
stateType: NimNode = nil
|
||||||
|
|
||||||
|
# By convention, all Ethereum protocol names must be abbreviated to 3 letters
|
||||||
|
assert protoName.len == 3
|
||||||
|
|
||||||
result = newNimNode(nnkStmtList)
|
result = newNimNode(nnkStmtList)
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
var `protocol` = `newProtocol`(`name`, `version`)
|
# One global variable per protocol holds the protocol run-time data
|
||||||
|
var `protocol` = `newProtocol`(`protoName`, `version`)
|
||||||
|
|
||||||
|
# Create a type actining as a pseudo-object representing the protocol (e.g. p2p)
|
||||||
|
type `protoNameIdent`* = object
|
||||||
|
|
||||||
|
# The protocol run-time data is available as a pseudo-field (e.g. `p2p.info`)
|
||||||
|
template info*(P: type `protoNameIdent`): ProtocolInfo = `protocol`
|
||||||
|
|
||||||
for n in body:
|
for n in body:
|
||||||
case n.kind
|
case n.kind
|
||||||
|
@ -298,9 +373,22 @@ macro rlpxProtocol*(name: static[string],
|
||||||
error("nextID expects a single int value", n)
|
error("nextID expects a single int value", n)
|
||||||
else:
|
else:
|
||||||
error(repr(n) & " is not a recognized call in RLPx protocol definitions", n)
|
error(repr(n) & " is not a recognized call in RLPx protocol definitions", n)
|
||||||
|
of nnkTypeSection:
|
||||||
|
if n.len == 1 and n[0][0].kind == nnkIdent and $n[0][0].ident == "State":
|
||||||
|
stateType = genSym(nskType, protoName & "State")
|
||||||
|
n[0][0] = stateType
|
||||||
|
result.add n
|
||||||
|
# Create a pseudo-field for the protocol State type (e.g. `p2p.State`)
|
||||||
|
result.add quote do:
|
||||||
|
template State*(P: type `protoNameIdent`): typedesc = `stateType`
|
||||||
|
else:
|
||||||
|
error("The only type that can be defined inside a RLPx protocol is the protocol's State type.")
|
||||||
|
|
||||||
of nnkProcDef:
|
of nnkProcDef:
|
||||||
inc nextId
|
inc nextId
|
||||||
let name = n.name.ident
|
let
|
||||||
|
msgIdent = n.name.ident
|
||||||
|
msgName = $msgIdent
|
||||||
|
|
||||||
var
|
var
|
||||||
thunkName = newNilLit()
|
thunkName = newNilLit()
|
||||||
|
@ -309,13 +397,14 @@ macro rlpxProtocol*(name: static[string],
|
||||||
peer = genSym(nskParam, "peer")
|
peer = genSym(nskParam, "peer")
|
||||||
|
|
||||||
if n.body.kind != nnkEmpty:
|
if n.body.kind != nnkEmpty:
|
||||||
# implement receiving thunk
|
# implement the receiving thunk proc that deserialzed the
|
||||||
|
# message parameters and calls the user proc:
|
||||||
var
|
var
|
||||||
nCopy = n.copyNimTree
|
nCopy = n.copyNimTree
|
||||||
rlp = genSym(nskParam, "rlp")
|
rlp = genSym(nskParam, "rlp")
|
||||||
connection = genSym(nskParam, "connection")
|
connection = genSym(nskParam, "connection")
|
||||||
|
|
||||||
nCopy.name = genSym(nskProc, $name)
|
nCopy.name = genSym(nskProc, msgName)
|
||||||
var callUserProc = newCall(nCopy.name, connection)
|
var callUserProc = newCall(nCopy.name, connection)
|
||||||
|
|
||||||
var readParams = newNimNode(nnkStmtList)
|
var readParams = newNimNode(nnkStmtList)
|
||||||
|
@ -333,24 +422,56 @@ macro rlpxProtocol*(name: static[string],
|
||||||
|
|
||||||
callUserProc.add deserializedParam
|
callUserProc.add deserializedParam
|
||||||
|
|
||||||
thunkName = newIdentNode($name & "_thunk")
|
thunkName = newIdentNode(msgName & "_thunk")
|
||||||
var thunk = quote do:
|
var thunk = quote do:
|
||||||
proc `thunkName`(`connection`: `Peer`, `rlp`: var Rlp) =
|
proc `thunkName`(`connection`: `Peer`, `rlp`: var Rlp) =
|
||||||
`readParams`
|
`readParams`
|
||||||
`callUserProc`
|
`callUserProc`
|
||||||
|
|
||||||
|
if stateType != nil:
|
||||||
|
# Define a local accessor for the current protocol state
|
||||||
|
# inside each handler (e.g. peer.state.foo = bar)
|
||||||
|
var localStateAccessor = quote:
|
||||||
|
template state(connection: `Peer`): ref `stateType` =
|
||||||
|
cast[ref `stateType`](connection.getState(`protocol`))
|
||||||
|
|
||||||
|
nCopy.body.insert 0, localStateAccessor
|
||||||
|
|
||||||
result.add nCopy, thunk
|
result.add nCopy, thunk
|
||||||
|
|
||||||
|
var
|
||||||
|
msgType = genSym(nskType, msgName & "Obj")
|
||||||
|
msgTypeFields = newTree(nnkRecList)
|
||||||
|
msgTypeBody = newTree(nnkObjectTy,
|
||||||
|
newEmptyNode(),
|
||||||
|
newEmptyNode(),
|
||||||
|
msgTypeFields)
|
||||||
|
|
||||||
# implement sending proc
|
# implement sending proc
|
||||||
for param, paramType in n.typedParams(skip = 1):
|
for param, paramType in n.typedParams(skip = 1):
|
||||||
appendParams.add quote do:
|
appendParams.add quote do:
|
||||||
`append`(`rlpWriter`, `param`)
|
`append`(`rlpWriter`, `param`)
|
||||||
|
|
||||||
|
msgTypeFields.add newTree(nnkIdentDefs,
|
||||||
|
param, chooseFieldType(paramType), newEmptyNode())
|
||||||
|
|
||||||
|
result.add quote do:
|
||||||
|
# This is a type featuring a single field for each message param:
|
||||||
|
type `msgType`* = `msgTypeBody`
|
||||||
|
|
||||||
|
# Add a helper template for accessing the message type:
|
||||||
|
# e.g. p2p.hello:
|
||||||
|
template `msgIdent`*(T: type `protoNameIdent`): typedesc = `msgType`
|
||||||
|
|
||||||
|
# Add a helper template for obtaining the message Id for
|
||||||
|
# a particular message type:
|
||||||
|
template msgId*(T: type `msgType`): int = `nextId`
|
||||||
|
|
||||||
# XXX TODO: check that the first param has the correct type
|
# XXX TODO: check that the first param has the correct type
|
||||||
n.params[1][0] = peer
|
n.params[1][0] = peer
|
||||||
|
|
||||||
let writeMsgId = if isSubprotocol:
|
let writeMsgId = if isSubprotocol:
|
||||||
quote: `writeMessageId`(`protocol`, `nextId`, `peer`, `rlpWriter`)
|
quote: `writeMsgId`(`protocol`, `nextId`, `peer`, `rlpWriter`)
|
||||||
else:
|
else:
|
||||||
quote: `append`(`rlpWriter`, `nextId`)
|
quote: `append`(`rlpWriter`, `nextId`)
|
||||||
|
|
||||||
|
@ -361,7 +482,7 @@ macro rlpxProtocol*(name: static[string],
|
||||||
`send`(`peer`, `finish`(`rlpWriter`))
|
`send`(`peer`, `finish`(`rlpWriter`))
|
||||||
|
|
||||||
result.add n
|
result.add n
|
||||||
result.add newCall(bindSym("registerMessage"),
|
result.add newCall(bindSym("registerMsg"),
|
||||||
protocol,
|
protocol,
|
||||||
newIntLitNode(nextId),
|
newIntLitNode(nextId),
|
||||||
newStrLitNode($n.name),
|
newStrLitNode($n.name),
|
||||||
|
@ -371,7 +492,7 @@ macro rlpxProtocol*(name: static[string],
|
||||||
error("illegal syntax in a RLPx protocol definition", n)
|
error("illegal syntax in a RLPx protocol definition", n)
|
||||||
|
|
||||||
result.add newCall(bindSym("registerProtocol"), protocol)
|
result.add newCall(bindSym("registerProtocol"), protocol)
|
||||||
echo repr(result)
|
when isMainModule: echo repr(result)
|
||||||
|
|
||||||
type
|
type
|
||||||
DisconnectionReason* = enum
|
DisconnectionReason* = enum
|
||||||
|
@ -389,16 +510,15 @@ type
|
||||||
MessageTimeout,
|
MessageTimeout,
|
||||||
SubprotocolReason = 0x10
|
SubprotocolReason = 0x10
|
||||||
|
|
||||||
rlpxProtocol("p2p", 0):
|
rlpxProtocol p2p, 0:
|
||||||
|
|
||||||
proc hello(peer: Peer,
|
proc hello(peer: Peer,
|
||||||
version: uint,
|
version: uint,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
capabilities: openarray[Protocol],
|
capabilities: openarray[Capability],
|
||||||
listenPort: uint,
|
listenPort: uint,
|
||||||
nodeId: MDigest[512]
|
nodeId: P2PNodeId) =
|
||||||
) =
|
peer.id = nodeId
|
||||||
discard
|
peer.dispatcher = getDispatcher(capabilities)
|
||||||
|
|
||||||
proc disconnect(peer: Peer, reason: DisconnectionReason)
|
proc disconnect(peer: Peer, reason: DisconnectionReason)
|
||||||
|
|
||||||
|
@ -407,6 +527,8 @@ rlpxProtocol("p2p", 0):
|
||||||
proc pong(peer: Peer) =
|
proc pong(peer: Peer) =
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
import typetraits
|
||||||
|
|
||||||
proc rlpxConnect*(myKeys: KeyPair, remoteKey: PublicKey,
|
proc rlpxConnect*(myKeys: KeyPair, remoteKey: PublicKey,
|
||||||
address: Address): Future[Peer] {.async.} =
|
address: Address): Future[Peer] {.async.} =
|
||||||
# TODO: Make sure to close the socket in case of exception
|
# TODO: Make sure to close the socket in case of exception
|
||||||
|
@ -438,29 +560,42 @@ proc rlpxConnect*(myKeys: KeyPair, remoteKey: PublicKey,
|
||||||
|
|
||||||
var ackMsg: array[AckMessageMaxEIP8, byte]
|
var ackMsg: array[AckMessageMaxEIP8, byte]
|
||||||
let ackMsgLen = handshake.ackSize(encrypt = encryptionEnabled)
|
let ackMsgLen = handshake.ackSize(encrypt = encryptionEnabled)
|
||||||
let receivedBytes = await result.socket.recvInto(addr ackMsg, ackMsgLen)
|
await result.socket.fullRecvInto(addr ackMsg, ackMsgLen)
|
||||||
|
|
||||||
if receivedBytes != ackMsgLen:
|
|
||||||
# XXX: this handling is not perfect, we should probably retry until the
|
|
||||||
# correct number of bytes are read!
|
|
||||||
raise newException(MalformedMessageError, "AuthAck message has incorrect size")
|
|
||||||
|
|
||||||
check handshake.decodeAckMessage(^ackMsg)
|
check handshake.decodeAckMessage(^ackMsg)
|
||||||
check handshake.getSecrets(^authMsg, ^ackMsg, result.sessionSecrets)
|
check handshake.getSecrets(^authMsg, ^ackMsg, result.sessionSecrets)
|
||||||
|
|
||||||
var
|
var
|
||||||
# XXX: TODO
|
# XXX: TODO: get these from somewhere
|
||||||
nodeId: MDigest[512]
|
nodeId: P2PNodeId
|
||||||
listeningPort = uint 0
|
listeningPort = uint 0
|
||||||
|
|
||||||
hello(result, baseProtocolVersion, clienId, gProtocols, listeningPort, nodeId)
|
hello(result, baseProtocolVersion, clienId, gCapabilities, listeningPort, nodeId)
|
||||||
|
|
||||||
|
var response = await result.nextMsg(p2p.hello, discardOthers = true)
|
||||||
|
result.dispatcher = getDispatcher(response.capabilities)
|
||||||
|
result.id = response.nodeId
|
||||||
|
result.connectionState = Connected
|
||||||
|
newSeq(result.protocolStates, gProtocols.len)
|
||||||
|
# XXX: initialize the sub-protocol states
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
import rlp
|
import rlp
|
||||||
|
|
||||||
rlpxProtocol("test", 1):
|
rlpxProtocol aaa, 1:
|
||||||
|
type State = object
|
||||||
|
peerName: string
|
||||||
|
|
||||||
|
proc hi(p: Peer, name: string) =
|
||||||
|
p.state.peerName = name
|
||||||
|
|
||||||
|
rlpxProtocol bbb, 1:
|
||||||
|
type State = object
|
||||||
|
messages: int
|
||||||
|
|
||||||
proc foo(p: Peer, s: string, a, z: int) =
|
proc foo(p: Peer, s: string, a, z: int) =
|
||||||
echo s
|
p.state.messages += 1
|
||||||
|
echo p.state(aaa).peerName
|
||||||
|
|
||||||
proc bar(p: Peer, i: int, s: string)
|
proc bar(p: Peer, i: int, s: string)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ type
|
||||||
header: BlockHeader
|
header: BlockHeader
|
||||||
body {.rlpInline.}: BlockBody
|
body {.rlpInline.}: BlockBody
|
||||||
|
|
||||||
rlpxProtocol("eth", 63):
|
rlpxProtocol eth, 63:
|
||||||
proc status(p: Peer, protocolVersion, networkId, td: P,
|
proc status(p: Peer, protocolVersion, networkId, td: P,
|
||||||
bestHash, genesisHash: KeccakHash) =
|
bestHash, genesisHash: KeccakHash) =
|
||||||
discard
|
discard
|
||||||
|
|
|
@ -30,7 +30,7 @@ type
|
||||||
status*: TransactionStatus
|
status*: TransactionStatus
|
||||||
data*: Blob
|
data*: Blob
|
||||||
|
|
||||||
rlpxProtocol("les", 2):
|
rlpxProtocol les, 2:
|
||||||
|
|
||||||
## Handshake
|
## Handshake
|
||||||
##
|
##
|
||||||
|
|
Loading…
Reference in New Issue