diff --git a/fluffy/rpc/rpc_discovery_api.nim b/fluffy/rpc/rpc_discovery_api.nim index c1bf766e8..a5517babd 100644 --- a/fluffy/rpc/rpc_discovery_api.nim +++ b/fluffy/rpc/rpc_discovery_api.nim @@ -9,27 +9,113 @@ import std/sequtils, - json_rpc/[rpcproxy, rpcserver], + json_rpc/[rpcproxy, rpcserver], stew/byteutils, eth/p2p/discoveryv5/protocol as discv5_protocol, ./rpc_types -proc installDiscoveryApiHandlers*(rpcServerWithProxy: var RpcProxy, +export rpc_types # tasty sandwich + +type + PongResponse = object + enrSeq: uint64 + recipientIP: string + recipientPort: uint16 + +proc installDiscoveryApiHandlers*(rpcServer: RpcServer|RpcProxy, d: discv5_protocol.Protocol) {.raises: [Defect, CatchableError].} = ## Discovery v5 JSON-RPC API such as defined here: ## https://ddht.readthedocs.io/en/latest/jsonrpc.html ## and here: ## https://github.com/ethereum/portal-network-specs/pull/88 - ## Note: There are quite some descrepencies between the two, can only - ## implement exactly once specification is settled. + ## Note: There are quite some discrepancies between the two, can only + ## implement exactly once specification has settled. - rpcServerWithProxy.rpc("discv5_nodeInfo") do() -> NodeInfo: - return d.routingTable.getNodeInfo() - - rpcServerWithProxy.rpc("discv5_routingTableInfo") do() -> RoutingTableInfo: + rpcServer.rpc("discv5_routingTableInfo") do() -> RoutingTableInfo: return getRoutingTableInfo(d.routingTable) - rpcServerWithProxy.rpc("discv5_recursiveFindNodes") do() -> seq[string]: + rpcServer.rpc("discv5_nodeInfo") do() -> NodeInfo: + return d.routingTable.getNodeInfo() + + rpcServer.rpc("discv5_updateNodeInfo") do( + kvPairs: seq[(string, string)]) -> NodeInfo: + let enrFields = kvPairs.map( + proc(n: (string, string)): (string, seq[byte]) = + (n[0], hexToSeqByte(n[1])) + ) + let updated = d.updateRecord(enrFields) + if updated.isErr(): + raise newException(ValueError, $updated.error) + + return d.routingTable.getNodeInfo() + + rpcServer.rpc("discv5_setEnr") do(enr: Record) -> bool: + if d.addNode(enr): + return true + else: + raise newException(ValueError, "Could not add node with this ENR to routing table") + + rpcServer.rpc("discv5_getEnr") do(nodeId: NodeId) -> Record: + let node = d.getNode(nodeId) + if node.isSome(): + return node.get().record + else: + raise newException(ValueError, "Record not in local routing table.") + + rpcServer.rpc("discv5_deleteEnr") do(nodeId: NodeId) -> bool: + # TODO: Adjust `removeNode` to accept NodeId as param and to return bool. + let node = d.getNode(nodeId) + if node.isSome(): + d.routingTable.removeNode(node.get()) + return true + else: + raise newException(ValueError, "Record not in local routing table.") + + rpcServer.rpc("discv5_lookupEnr") do(nodeId: NodeId) -> Record: + # TODO: Not using seqNum, what is the use case of this? + let lookup = await d.resolve(nodeId) + if lookup.isSome(): + return lookup.get().record + else: + raise newException(ValueError, "Record not found in DHT lookup.") + + rpcServer.rpc("discv5_ping") do(enr: Record) -> PongResponse: + let + node = toNodeWithAddress(enr) + pong = await d.ping(node) + + if pong.isErr(): + raise newException(ValueError, $pong.error) + else: + let p = pong.get() + return PongResponse( + enrSeq: p.enrSeq, + recipientIP: $p.ip, + recipientPort: p.port + ) + + rpcServer.rpc("discv5_findNodes") do( + enr: Record, distances: seq[uint16]) -> seq[Record]: + let + node = toNodeWithAddress(enr) + nodes = await d.findNode(node, distances) + if nodes.isErr(): + raise newException(ValueError, $nodes.error) + else: + return nodes.get().map(proc(n: Node): Record = n.record) + + rpcServer.rpc("discv5_talk") do(enr: Record, protocol, payload: string) -> string: + let + node = toNodeWithAddress(enr) + talkresp = await d.talkreq( + node, hexToSeqByte(protocol), hexToSeqByte(payload)) + if talkresp.isErr(): + raise newException(ValueError, $talkresp.error) + else: + return talkresp.get().toHex() + + rpcServer.rpc("discv5_recursiveFindNodes") do() -> seq[Record]: # TODO: Not according to the specification currently. Should do a lookup # here instead of query, and the node_id is a parameter to be passed. + # But in that case it would be very similar to discv5_lookupEnr. let discovered = await d.queryRandom() - return discovered.map(proc(n: Node): string = n.record.toURI()) + return discovered.map(proc(n: Node): Record = n.record) diff --git a/fluffy/rpc/rpc_portal_api.nim b/fluffy/rpc/rpc_portal_api.nim index 09ac9530b..fc41b4285 100644 --- a/fluffy/rpc/rpc_portal_api.nim +++ b/fluffy/rpc/rpc_portal_api.nim @@ -8,6 +8,7 @@ {.push raises: [Defect].} import + std/sequtils, json_rpc/[rpcproxy, rpcserver], ../network/wire/portal_protocol, ./rpc_types @@ -21,14 +22,18 @@ export rpcserver # as the proc becomes generic, where the rpc macro from router.nim can no longer # be found, which is why we export rpcserver which should export router. proc installPortalApiHandlers*( - rpcServerWithProxy: var RpcProxy, p: PortalProtocol, network: static string) + rpcServer: RpcServer|RpcProxy, p: PortalProtocol, network: static string) {.raises: [Defect, CatchableError].} = ## Portal routing table and portal wire json-rpc API is not yet defined but ## will look something similar as what exists here now: ## https://github.com/ethereum/portal-network-specs/pull/88 - rpcServerWithProxy.rpc("portal_" & network & "_nodeInfo") do() -> NodeInfo: + rpcServer.rpc("portal_" & network & "_nodeInfo") do() -> NodeInfo: return p.routingTable.getNodeInfo() - rpcServerWithProxy.rpc("portal_" & network & "_routingTableInfo") do() -> RoutingTableInfo: + rpcServer.rpc("portal_" & network & "_routingTableInfo") do() -> RoutingTableInfo: return getRoutingTableInfo(p.routingTable) + + rpcServer.rpc("portal_" & network & "_recursiveFindNodes") do() -> seq[Record]: + let discovered = await p.queryRandom() + return discovered.map(proc(n: Node): Record = n.record) diff --git a/fluffy/rpc/rpc_types.nim b/fluffy/rpc/rpc_types.nim index 0f7c859b0..ba8682726 100644 --- a/fluffy/rpc/rpc_types.nim +++ b/fluffy/rpc/rpc_types.nim @@ -8,31 +8,79 @@ {.push raises: [Defect].} import + json_rpc/jsonmarshal, + stew/results, eth/p2p/discoveryv5/[routing_table, enr, node] +export jsonmarshal, enr, routing_table + type NodeInfo* = object - nodeId: string - nodeENR: string + nodeId: NodeId + nodeENR: Record RoutingTableInfo* = object - localKey: string - buckets: seq[seq[string]] + localKey: NodeId + buckets: seq[seq[NodeId]] proc getNodeInfo*(r: RoutingTable): NodeInfo = - let id = "0x" & r.localNode.id.toHex() - let enr = r.localNode.record.toURI() - return NodeInfo(nodeId: id, nodeENR: enr) + NodeInfo(nodeId: r.localNode.id, nodeENR: r.localNode.record) proc getRoutingTableInfo*(r: RoutingTable): RoutingTableInfo = var info: RoutingTableInfo for b in r.buckets: - var bucket: seq[string] + var bucket: seq[NodeId] for n in b.nodes: - bucket.add("0x" & n.id.toHex()) + bucket.add(n.id) info.buckets.add(bucket) - info.localKey = "0x" & r.localNode.id.toHex() + info.localKey = r.localNode.id info + +proc toNodeWithAddress*(enr: Record): Node {.raises: [Defect, ValueError].} = + let nodeRes = newNode(enr) + if nodeRes.isErr(): + raise newException(ValueError, $nodeRes.error) + + let node = nodeRes.get() + if node.address.isNone(): + raise newException(ValueError, "ENR without address") + else: + node + +proc `%`*(value: Record): JsonNode = + newJString(value.toURI()) + +proc fromJson*(n: JsonNode, argName: string, result: var Record) + {.raises: [Defect, ValueError].} = + n.kind.expect(JString, argName) + if not fromURI(result, n.getStr()): + raise newException(ValueError, "Invalid ENR") + +proc `%`*(value: NodeId): JsonNode = + %("0x" & value.toHex()) + +proc fromJson*(n: JsonNode, argName: string, result: var NodeId) + {.raises: [Defect, ValueError].} = + n.kind.expect(JString, argName) + + # TODO: fromHex (and thus parse) call seems to let pass several invalid + # UInt256. + result = UInt256.fromHex(n.getStr()) + +# TODO: This one should go to nim-json-rpc but before we can do that we will +# have to update the vendor module to the current latest. +proc fromJson*(n: JsonNode, argName: string, result: var uint16) + {.raises: [Defect, ValueError].} = + n.kind.expect(JInt, argName) + let asInt = n.getBiggestInt() + if asInt < 0: + raise newException( + ValueError, "JSON-RPC input is an unexpected negative value") + if asInt > BiggestInt(uint16.high()): + raise newException( + ValueError, "JSON-RPC input is too large for uint32") + + result = uint16(asInt) diff --git a/fluffy/scripts/launch_local_testnet.sh b/fluffy/scripts/launch_local_testnet.sh index f9eb77c5a..5f4470d12 100755 --- a/fluffy/scripts/launch_local_testnet.sh +++ b/fluffy/scripts/launch_local_testnet.sh @@ -51,7 +51,7 @@ SCRIPTS_DIR="fluffy/scripts/" print_help() { cat <