From d43f20c65acd0cbd34d84e3ca380e5316340ab5f Mon Sep 17 00:00:00 2001 From: kdeme Date: Sat, 23 Mar 2019 21:54:28 +0100 Subject: [PATCH] Initial implementation of Whisper RPC --- nimbus/nimbus.nim | 3 +- nimbus/rpc/hexstrings.nim | 116 +++++++--- nimbus/rpc/rpc_types.nim | 70 +++--- nimbus/rpc/whisper.nim | 387 +++++++++++++++++++++++++++----- tests/rpcclient/ethcallsigs.nim | 25 +++ tests/test_rpc_whisper.nim | 142 ++++++++++++ 6 files changed, 632 insertions(+), 111 deletions(-) create mode 100644 tests/test_rpc_whisper.nim diff --git a/nimbus/nimbus.nim b/nimbus/nimbus.nim index ebb067f07..2d4f03326 100644 --- a/nimbus/nimbus.nim +++ b/nimbus/nimbus.nim @@ -108,7 +108,8 @@ proc start(): NimbusObject = if RpcFlags.Eth in conf.rpc.flags and ProtocolFlags.Eth in conf.net.protocols: setupEthRpc(nimbus.ethNode, chainDB, nimbus.rpcServer) if RpcFlags.Shh in conf.rpc.flags and ProtocolFlags.Shh in conf.net.protocols: - setupWhisperRPC(nimbus.rpcServer) + let keys = newWhisperKeys() + setupWhisperRPC(nimbus.ethNode, keys, nimbus.rpcServer) if RpcFlags.Debug in conf.rpc.flags: setupDebugRpc(chainDB, nimbus.rpcServer) diff --git a/nimbus/rpc/hexstrings.nim b/nimbus/rpc/hexstrings.nim index 781a00750..cec7fda2e 100644 --- a/nimbus/rpc/hexstrings.nim +++ b/nimbus/rpc/hexstrings.nim @@ -20,17 +20,30 @@ * seq[byte] * openArray[seq] * ref BloomFilter + * PublicKey + * PrivateKey + * SymKey + * Topic + * Bytes ]# -import eth/common/eth_types, stint, byteutils, nimcrypto +import + stint, byteutils, eth/[keys, rlp], eth/common/eth_types, + eth/p2p/rlpx_protocols/whisper_protocol type HexQuantityStr* = distinct string HexDataStr* = distinct string EthAddressStr* = distinct string # Same as HexDataStr but must be less <= 20 bytes EthHashStr* = distinct string # Same as HexDataStr but must be exactly 32 bytes - WhisperIdentityStr* = distinct string # 60 bytes - HexStrings = HexQuantityStr | HexDataStr | EthAddressStr | EthHashStr | WhisperIdentityStr + IdentifierStr* = distinct string # 32 bytes, no 0x prefix! + PublicKeyStr* = distinct string # 0x prefix + 65 bytes + PrivateKeyStr* = distinct string # 0x prefix + 32 bytes + SymKeyStr* = distinct string # 0x prefix + 32 bytes + TopicStr* = distinct string # 0x prefix + 4 bytes + HexStrings = HexQuantityStr | HexDataStr | EthAddressStr | EthHashStr | + IdentifierStr | PublicKeyStr | PrivateKeyStr | SymKeyStr | + TopicStr template len*(value: HexStrings): int = value.string.len @@ -68,8 +81,8 @@ func isValidHexQuantity*(value: string): bool = return false return true -func isValidHexData*(value: string): bool = - if not value.hasHexHeader: +func isValidHexData*(value: string, header = true): bool = + if header and not value.hasHexHeader: return false # Must be even number of digits if value.len mod 2 != 0: return false @@ -91,17 +104,36 @@ func isValidEthHash*(value: string): bool = # TODO: Allow shorter hashes (pad with zeros) for convenience? result = value.len == 66 and value.isValidHexData -func isValidWhisperIdentity*(value: string): bool = - # 60 bytes for WhisperIdentity plus "0x" - # TODO: Are the HexData constratins applicable to Whisper identities? - result = value.len == 122 and value.isValidHexData +func isValidIdentifier*(value: string): bool = + # 32 bytes for Whisper ID, no 0x prefix + result = value.len == 64 and value.isvalidHexData(header = false) + +func isValidPublicKey*(value: string): bool = + # 65 bytes for Public Key plus 1 byte for 0x prefix + result = value.len == 132 and value.isValidHexData + +func isValidPrivateKey*(value: string): bool = + # 32 bytes for Private Key plus 1 byte for 0x prefix + result = value.len == 66 and value.isValidHexData + +func isValidSymKey*(value: string): bool = + # 32 bytes for Private Key plus 1 byte for 0x prefix + result = value.len == 66 and value.isValidHexData + +func isValidTopic*(value: string): bool = + # 4 bytes for Topic plus 1 byte for 0x prefix + result = value.len == 10 and value.isValidHexData const SInvalidQuantity = "Invalid hex quantity format for Ethereum" SInvalidData = "Invalid hex data format for Ethereum" SInvalidAddress = "Invalid address format for Ethereum" SInvalidHash = "Invalid hash format for Ethereum" - SInvalidWhisperIdentity = "Invalid format for whisper identity" + SInvalidIdentifier = "Invalid format for identifier" + SInvalidPublicKey = "Invalid format for public key" + SInvalidPrivateKey = "Invalid format for private key" + SInvalidSymKey = "Invalid format for symmetric key" + SInvalidTopic = "Invalid format for topic" proc validateHexQuantity*(value: string) {.inline.} = if unlikely(not value.isValidHexQuantity): @@ -119,10 +151,6 @@ proc validateHashStr*(value: string) {.inline.} = if unlikely(not value.isValidEthHash): raise newException(ValueError, SInvalidHash & ": " & value) -proc validateWhisperIdentity*(value: string) {.inline.} = - if unlikely(not value.isValidWhisperIdentity): - raise newException(ValueError, SInvalidWhisperIdentity & ": " & value) - # Initialisation proc hexQuantityStr*(value: string): HexQuantityStr {.inline.} = @@ -141,10 +169,6 @@ proc ethHashStr*(value: string): EthHashStr {.inline.} = value.validateHashStr result = value.EthHashStr -proc whisperIdentity*(value: string): WhisperIdentityStr {.inline.} = - value.validateWhisperIdentity - result = value.WhisperIdentityStr - # Converters for use in RPC import json @@ -162,17 +186,30 @@ proc `%`*(value: ref EthAddress): JsonNode = result = %("0x" & value[].toHex) proc `%`*(value: Hash256): JsonNode = - result = %("0x" & $value) + #result = %("0x" & $value) # More clean but no lowercase :( + result = %("0x" & value.data.toHex) proc `%`*(value: UInt256): JsonNode = result = %("0x" & value.toString) -proc `%`*(value: WhisperIdentity): JsonNode = - result = %("0x" & byteutils.toHex(value)) - proc `%`*(value: ref BloomFilter): JsonNode = result = %("0x" & toHex[256](value[])) +proc `%`*(value: PublicKey): JsonNode = + result = %("0x04" & $value) + +proc `%`*(value: PrivateKey): JsonNode = + result = %("0x" & $value) + +proc `%`*(value: SymKey): JsonNode = + result = %("0x" & value.toHex) + +proc `%`*(value: whisper_protocol.Topic): JsonNode = + result = %("0x" & value.toHex) + +proc `%`*(value: Bytes): JsonNode = + result = %("0x" & value.toHex) + # Marshalling from JSON to Nim types that includes format checking func invalidMsg(name: string): string = "When marshalling from JSON, parameter \"" & name & "\" is not valid" @@ -205,12 +242,12 @@ proc fromJson*(n: JsonNode, argName: string, result: var EthHashStr) = raise newException(ValueError, invalidMsg(argName) & " as an Ethereum hash \"" & hexStr & "\"") result = hexStr.EthHashStr -proc fromJson*(n: JsonNode, argName: string, result: var WhisperIdentityStr) = +proc fromJson*(n: JsonNode, argName: string, result: var IdentifierStr) = n.kind.expect(JString, argName) let hexStr = n.getStr() - if not hexStr.isValidWhisperIdentity: - raise newException(ValueError, invalidMsg(argName) & " as a Whisper identity \"" & hexStr & "\"") - result = hexStr.WhisperIdentityStr + if not hexStr.isValidIdentifier: + raise newException(ValueError, invalidMsg(argName) & " as a identifier \"" & hexStr & "\"") + result = hexStr.IdentifierStr proc fromJson*(n: JsonNode, argName: string, result: var UInt256) = n.kind.expect(JString, argName) @@ -219,3 +256,30 @@ proc fromJson*(n: JsonNode, argName: string, result: var UInt256) = raise newException(ValueError, invalidMsg(argName) & " as a UInt256 \"" & hexStr & "\"") result = readUintBE[256](hexToPaddedByteArray[32](hexStr)) +proc fromJson*(n: JsonNode, argName: string, result: var PublicKeyStr) = + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidPublicKey: + raise newException(ValueError, invalidMsg(argName) & " as a public key \"" & hexStr & "\"") + result = hexStr.PublicKeyStr + +proc fromJson*(n: JsonNode, argName: string, result: var PrivateKeyStr) = + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidPrivateKey: + raise newException(ValueError, invalidMsg(argName) & " as a private key \"" & hexStr & "\"") + result = hexStr.PrivateKeyStr + +proc fromJson*(n: JsonNode, argName: string, result: var SymKeyStr) = + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidSymKey: + raise newException(ValueError, invalidMsg(argName) & " as a symmetric key \"" & hexStr & "\"") + result = hexStr.SymKeyStr + +proc fromJson*(n: JsonNode, argName: string, result: var TopicStr) = + n.kind.expect(JString, argName) + let hexStr = n.getStr() + if not hexStr.isValidTopic: + raise newException(ValueError, invalidMsg(argName) & " as a topic \"" & hexStr & "\"") + result = hexStr.TopicStr diff --git a/nimbus/rpc/rpc_types.nim b/nimbus/rpc/rpc_types.nim index 83301be8d..0143297c8 100644 --- a/nimbus/rpc/rpc_types.nim +++ b/nimbus/rpc/rpc_types.nim @@ -1,4 +1,6 @@ -import eth/common, hexstrings, options +import + hexstrings, options, eth/[common, keys, rlp], + eth/p2p/rlpx_protocols/whisper_protocol #[ Notes: @@ -26,7 +28,7 @@ type gasPrice*: GasInt # (optional, default: To-Be-Determined) integer of the gasPrice used for each paid gas. value*: UInt256 # (optional) integer of the value sent with this transaction. data*: EthHashStr # TODO: Support more data. The compiled code of a contract OR the hash of the invoked method signature and encoded parameters. For details see Ethereum Contract ABI. - nonce*: AccountNonce # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce + nonce*: AccountNonce # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce EthCall* = object # Parameter from user @@ -119,28 +121,46 @@ type toBlock*: Option[string] # (optional, default: "latest") integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions. address*: Option[EthAddress] # (optional) contract address or a list of addresses from which logs should originate. topics*: Option[seq[FilterData]] # (optional) list of DATA topics. Topics are order-dependent. Each topic can also be a list of DATA with "or" options. - - WhisperFilterOptions* = object - to*: Option[WhisperIdentityStr] - topics*: seq[HexDataStr] - WhisperPost* = object - # Parameter from user - source*: Option[WhisperIdentityStr] # (optional) the identity of the sender. - to*: Option[WhisperIdentityStr] # (optional) the identity of the receiver. When present whisper will encrypt the message so that only the receiver can decrypt it. - topics*: seq[HexDataStr] # list of DATA topics, for the receiver to identify messages. - payload*: HexDataStr # the payload of the message. - priority*: int # integer of the priority in a rang from. - ttl*: int # integer of the time to live in seconds. - - WhisperMessage* = object + WhisperInfo* = object # Returned to user - hash*: Hash256 # the hash of the message. - source*: WhisperIdentity # the sender of the message, if a sender was specified. - to*: WhisperIdentity # the receiver of the message, if a receiver was specified. - expiry*: int # integer of the time in seconds when this message should expire. - ttl*: int # integer of the time the message should float in the system in seconds. - sent*: int # integer of the unix timestamp when the message was sent. - topics*: seq[UInt256] # list of DATA topics the message contained. - payload*: Blob # the payload of the message. - workProved*: int # integer of the work this message required before it was send. + minPow*: float64 # Current minimum PoW requirement. + # TODO: may be uint32 + maxMessageSize*: uint64 # Current message size limit in bytes. + memory*: int # Memory size of the floating messages in bytes. + messages*: int # Number of floating messages. + + WhisperFilterOptions* = object + # Parameter from user + symKeyID*: Option[IdentifierStr] # ID of symmetric key for message decryption. + privateKeyID*: Option[IdentifierStr] # ID of private (asymmetric) key for message decryption. + sig*: Option[PublicKeyStr] # (Optional) Public key of the signature. + minPow*: Option[float64] # (Optional) Minimal PoW requirement for incoming messages. + topics*: Option[seq[TopicStr]] # (Optional when asym key): Array of possible topics (or partial topics). + allowP2P*: Option[bool] # (Optional) Indicates if this filter allows processing of direct peer-to-peer messages. + + WhisperFilterMessage* = object + # Returned to user + sig*: Option[PublicKey] # Public key who signed this message. + recipientPublicKey*: Option[PublicKey] # The recipients public key. + ttl*: uint64 # Time-to-live in seconds. + timestamp*: uint64 # Unix timestamp of the message generation. + topic*: whisper_protocol.Topic # 4 Bytes: Message topic. + payload*: Bytes # Decrypted payload. + padding*: Bytes # (Optional) Padding (byte array of arbitrary length). + pow*: float64 # Proof of work value. + hash*: Hash # Hash of the enveloped message. + + WhisperPostMessage* = object + # Parameter from user + symKeyID*: Option[IdentifierStr] # ID of symmetric key for message encryption. + pubKey*: Option[PublicKeyStr] # Public key for message encryption. + sig*: Option[IdentifierStr] # (Optional) ID of the signing key. + ttl*: uint64 # Time-to-live in seconds. + topic*: Option[TopicStr] # Message topic (mandatory when key is symmetric). + payload*: HexDataStr # Payload to be encrypted. + padding*: Option[HexDataStr] # (Optional) Padding (byte array of arbitrary length). + powTime*: float64 # Maximal time in seconds to be spent on proof of work. + powTarget*: float64 # Minimal PoW target required for this message. + # TODO: EnodeStr + targetPeer*: Option[string] # (Optional) Peer ID (for peer-to-peer message only). diff --git a/nimbus/rpc/whisper.nim b/nimbus/rpc/whisper.nim index b398d5983..4c2df8e38 100644 --- a/nimbus/rpc/whisper.nim +++ b/nimbus/rpc/whisper.nim @@ -1,74 +1,343 @@ -import json_rpc/rpcserver, rpc_types, stint, hexstrings, eth/common +import + json_rpc/rpcserver, rpc_types, hexstrings, tables, options, sequtils, + eth/[common, rlp, keys, p2p], eth/p2p/rlpx_protocols/whisper_protocol + +from nimcrypto/sysrand import randomBytes +from byteutils import hexToSeqByte, hexToByteArray + +# Whisper RPC implemented mostly as in +# https://github.com/ethereum/go-ethereum/wiki/Whisper-v6-RPC-API + +# TODO: rpc calls -> check all return values and matching documentation + +type + WhisperKeys* = ref object + asymKeys*: Table[string, KeyPair] + symKeys*: Table[string, SymKey] + +proc newWhisperKeys*(): WhisperKeys = + new(result) + result.asymKeys = initTable[string, KeyPair]() + result.symKeys = initTable[string, SymKey]() + +proc setupWhisperRPC*(node: EthereumNode, keys: WhisperKeys, rpcsrv: RpcServer) = -proc setupWhisperRPC*(rpcsrv: RpcServer) = rpcsrv.rpc("shh_version") do() -> string: ## Returns string of the current whisper protocol version. + result = whisperVersionStr + + rpcsrv.rpc("shh_info") do() -> WhisperInfo: + ## Returns diagnostic information about the whisper node. + let config = node.protocolState(Whisper).config + result = WhisperInfo(minPow: config.powRequirement, + maxMessageSize: config.maxMsgSize, + memory: 0, + messages: 0) + + # TODO: uint32 instead of uint64 is OK here, but needs to be added in json_rpc + rpcsrv.rpc("shh_setMaxMessageSize") do(size: uint64) -> bool: + ## Sets the maximal message size allowed by this node. + ## Incoming and outgoing messages with a larger size will be rejected. + ## Whisper message size can never exceed the limit imposed by the underlying + ## P2P protocol (10 Mb). + ## + ## size: Message size in bytes. + ## + ## Returns true on success and an error on failure. + result = node.setMaxMessageSize(size.uint32) + + rpcsrv.rpc("shh_setMinPoW") do(pow: float) -> bool: + ## Sets the minimal PoW required by this node. + ## + ## pow: The new PoW requirement. + ## + ## Returns true on success and an error on failure. + # TODO: is asyncCheck here OK? + asyncCheck node.setPowRequirement(pow) + result = true + + # TODO: change string in to ENodeStr with extra checks + rpcsrv.rpc("shh_markTrustedPeer") do(enode: string) -> bool: + ## Marks specific peer trusted, which will allow it to send historic + ## (expired) messages. + ## Note: This function is not adding new nodes, the node needs to exists as + ## a peer. + ## + ## enode: Enode of the trusted peer. + ## + ## Returns true on success and an error on failure. + # TODO: It will now require an enode://pubkey@ip:port uri + # could also accept only the pubkey (like geth)? + let peerNode = newNode(enode) + result = node.setPeerTrusted(peerNode.id) + + rpcsrv.rpc("shh_newKeyPair") do() -> IdentifierStr: + ## Generates a new public and private key pair for message decryption and + ## encryption. + ## + ## Returns key identifier on success and an error on failure. + result = generateRandomID().IdentifierStr + keys.asymKeys.add(result.string, newKeyPair()) + + rpcsrv.rpc("shh_addPrivateKey") do(key: PrivateKeyStr) -> IdentifierStr: + ## Stores the key pair, and returns its ID. + ## + ## key: Private key as hex bytes. + ## + ## Returns key identifier on success and an error on failure. + result = generateRandomID().IdentifierStr + + # No need to check if 0x prefix as the JSON Marshalling should handle this + var privkey = initPrivateKey(key.string[2 .. ^1]) + keys.asymKeys.add(result.string, KeyPair(seckey: privkey, + pubkey: privkey.getPublicKey())) + + rpcsrv.rpc("shh_deleteKeyPair") do(id: IdentifierStr) -> bool: + ## Deletes the specifies key if it exists. + ## + ## id: Identifier of key pair + ## + ## Returns true on success and an error on failure. + var unneeded: KeyPair + result = keys.asymKeys.take(id.string, unneeded) + + rpcsrv.rpc("shh_hasKeyPair") do(id: IdentifierStr) -> bool: + ## Checks if the whisper node has a private key of a key pair matching the + ## given ID. + ## + ## id: Identifier of key pair + ## + ## Returns true on success and an error on failure. + result = keys.asymkeys.hasKey(id.string) + + rpcsrv.rpc("shh_getPublicKey") do(id: IdentifierStr) -> PublicKey: + ## Returns the public key for identity ID. + ## + ## id: Identifier of key pair + ## + ## Returns public key on success and an error on failure. + # Note: key not found exception as error in case not existing + result = keys.asymkeys[id.string].pubkey + + rpcsrv.rpc("shh_getPrivateKey") do(id: IdentifierStr) -> PrivateKey: + ## Returns the private key for identity ID. + ## + ## id: Identifier of key pair + ## + ## Returns private key on success and an error on failure. + # Note: key not found exception as error in case not existing + result = keys.asymkeys[id.string].seckey + + rpcsrv.rpc("shh_newSymKey") do() -> IdentifierStr: + ## Generates a random symmetric key and stores it under an ID, which is then + ## returned. Can be used encrypting and decrypting messages where the key is + ## known to both parties. + ## + ## Returns key identifier on success and an error on failure. + result = generateRandomID().IdentifierStr + var key: SymKey + if randomBytes(key) != key.len: + error "Generation of SymKey failed" + + keys.symKeys.add(result.string, key) + + + rpcsrv.rpc("shh_addSymKey") do(key: SymKeyStr) -> IdentifierStr: + ## Stores the key, and returns its ID. + ## + ## key: The raw key for symmetric encryption as hex bytes. + ## + ## Returns key identifier on success and an error on failure. + result = generateRandomID().IdentifierStr + + var symKey: SymKey + # No need to check if 0x prefix as the JSON Marshalling should handle this + hexToByteArray(key.string[2 .. ^1], symKey) + keys.symKeys.add(result.string, symKey) + + rpcsrv.rpc("shh_generateSymKeyFromPassword") do(password: string) -> IdentifierStr: + ## Generates the key from password, stores it, and returns its ID. + ## + ## password: Password. + ## + ## Returns key identifier on success and an error on failure. + # TODO: implement, can use nimcrypto/pbkdf2 discard - rpcsrv.rpc("shh_post") do(message: WhisperPost) -> bool: - ## Sends a whisper message. + rpcsrv.rpc("shh_hasSymKey") do(id: IdentifierStr) -> bool: + ## Returns true if there is a key associated with the name string. + ## Otherwise, returns false. + ## + ## id: Identifier of key. + ## + ## Returns (true or false) on success and an error on failure. + result = keys.symkeys.hasKey(id.string) + + rpcsrv.rpc("shh_getSymKey") do(id: IdentifierStr) -> SymKey: + ## Returns the symmetric key associated with the given ID. + ## + ## id: Identifier of key. + ## + ## Returns Raw key on success and an error on failure. + # Note: key not found exception as error in case not existing + result = keys.symkeys[id.string] + + rpcsrv.rpc("shh_deleteSymKey") do(id: IdentifierStr) -> bool: + ## Deletes the key associated with the name string if it exists. + ## + ## id: Identifier of key. + ## + ## Returns (true or false) on success and an error on failure. + var unneeded: SymKey + result = keys.symKeys.take(id.string, unneeded) + + rpcsrv.rpc("shh_subscribe") do(id: string, + options: WhisperFilterOptions) -> IdentifierStr: + ## Creates and registers a new subscription to receive notifications for + ## inbound whisper messages. Returns the ID of the newly created + ## subscription. + ## + ## id: identifier of function call. In case of Whisper must contain the + ## value "messages". + ## options: WhisperFilterOptions + ## + ## Returns the subscription ID on success, the error on failure. + + # TODO: implement subscriptions, only for WS & IPC? + discard + + rpcsrv.rpc("shh_unsubscribe") do(id: IdentifierStr) -> bool: + ## Cancels and removes an existing subscription. + ## + ## id: Subscription identifier + ## + ## Returns (true or false) on success, the error on failure + result = node.unsubscribeFilter(id.string) + + proc validateOptions[T,U,V](asym: Option[T], sym: Option[U], topic: Option[V]) = + if (asym.isSome() and sym.isSome()) or (asym.isNone() and sym.isNone()): + raise newException(ValueError, + "Either privateKeyID/pubKey or symKeyID must be present") + if asym.isNone() and topic.isNone(): + raise newException(ValueError, "Topic mandatory with symmetric key") + + rpcsrv.rpc("shh_newMessageFilter") do(options: WhisperFilterOptions) -> IdentifierStr: + ## Create a new filter within the node. This filter can be used to poll for + ## new messages that match the set of criteria. + ## + ## options: WhisperFilterOptions + ## + ## Returns filter identifier on success, error on failure + + # Check if either symKeyID or privateKeyID is present, and not both + # Check if there are Topics when symmetric key is used + validateOptions(options.privateKeyID, options.symKeyID, options.topics) + + var filter: Filter + if options.privateKeyID.isSome(): + filter.privateKey = some(keys.asymKeys[options.privateKeyID.get().string].seckey) + + if options.symKeyID.isSome(): + filter.symKey= some(keys.symKeys[options.symKeyID.get().string]) + + if options.sig.isSome(): + # Need to strip 0x04 + filter.src = some(initPublicKey(options.sig.get().string[4 .. ^1])) + + if options.minPow.isSome(): + filter.powReq = options.minPow.get() + + if options.topics.isSome(): + filter.topics = map(options.topics.get(), + proc(x: TopicStr): whisper_protocol.Topic = + hexToByteArray(x.string[2 .. ^1], result)) + + if options.allowP2P.isSome(): + filter.allowP2P = options.allowP2P.get() + + result = node.subscribeFilter(filter).IdentifierStr + + rpcsrv.rpc("shh_deleteMessageFilter") do(id: IdentifierStr) -> bool: + ## Uninstall a message filter in the node. + ## + ## id: Filter identifier as returned when the filter was created. + ## + ## Returns true on success, error on failure. + result = node.unsubscribeFilter(id.string) + + rpcsrv.rpc("shh_getFilterMessages") do(id: IdentifierStr) -> seq[WhisperFilterMessage]: + ## Retrieve messages that match the filter criteria and are received between + ## the last time this function was called and now. + ## + ## id: ID of filter that was created with `shh_newMessageFilter`. + ## + ## Returns array of messages on success and an error on failure. + let messages = node.getFilterMessages(id.string) + for msg in messages: + var filterMsg: WhisperFilterMessage + + if msg.decoded.src.isSome(): + filterMsg.sig = some(msg.decoded.src.get()) + if msg.dst.isSome(): + filterMsg.recipientPublicKey = some(msg.dst.get()) + filterMsg.ttl = msg.ttl + filterMsg.topic = msg.topic + filterMsg.timestamp = msg.timestamp + filterMsg.payload = msg.decoded.payload + # TODO: could also remove the Option on padding in whisper_protocol? + if msg.decoded.padding.isSome(): + filterMsg.padding = msg.decoded.padding.get() + filterMsg.pow = msg.pow + filterMsg.hash = msg.hash + + result.add(filterMsg) + + rpcsrv.rpc("shh_post") do(message: WhisperPostMessage) -> bool: + ## Creates a whisper message and injects it into the network for + ## distribution. ## ## message: Whisper message to post. - ## Returns true if the message was send, otherwise false. - discard - - rpcsrv.rpc("shh_newIdentity") do() -> WhisperIdentity: - ## Creates new whisper identity in the client. ## - ## Returns the address of the new identiy. - discard + ## Returns true on success and an error on failure. - rpcsrv.rpc("shh_hasIdentity") do(identity: WhisperIdentityStr) -> bool: - ## Checks if the client holds the private keys for a given identity. - ## - ## identity: the identity address to check. - ## Returns true if the client holds the privatekey for that identity, otherwise false. - discard + # Check if either symKeyID or pubKey is present, and not both + # Check if there is a Topic when symmetric key is used + validateOptions(message.pubKey, message.symKeyID, message.topic) - rpcsrv.rpc("shh_newGroup") do() -> WhisperIdentity: - ## (?) - This has no description information in the RPC wiki. - ## - ## Returns the address of the new group. (?) - discard + var + pubKey: Option[PublicKey] + sigPrivKey: Option[PrivateKey] + symKey: Option[SymKey] + topic: whisper_protocol.Topic + padding: Option[Bytes] + targetPeer: Option[NodeId] - rpcsrv.rpc("shh_addToGroup") do(identity: WhisperIdentityStr) -> bool: - ## (?) - This has no description information in the RPC wiki. - ## - ## identity: the identity address to add to a group (?). - ## Returns true if the identity was successfully added to the group, otherwise false (?). - discard + if message.pubKey.isSome(): + pubKey = some(initPublicKey(message.pubKey.get().string[4 .. ^1])) - rpcsrv.rpc("shh_newFilter") do(filterOptions: WhisperFilterOptions) -> int: - ## Creates filter to notify, when client receives whisper message matching the filter options. - ## - ## filterOptions: The filter options: - ## to: DATA, 60 Bytes - (optional) identity of the receiver. When present it will try to decrypt any incoming message if the client holds the private key to this identity. - ## topics: Array of DATA - list of DATA topics which the incoming message's topics should match. You can use the following combinations: - ## [A, B] = A && B - ## [A, [B, C]] = A && (B || C) - ## [null, A, B] = ANYTHING && A && B null works as a wildcard - ## Returns the newly created filter. - discard + if message.sig.isSome(): + sigPrivKey = some(keys.asymKeys[message.sig.get().string].seckey) - rpcsrv.rpc("shh_uninstallFilter") do(id: int) -> bool: - ## Uninstalls a filter with given id. - ## Should always be called when watch is no longer needed. - ## Additonally Filters timeout when they aren't requested with shh_getFilterChanges for a period of time. - ## - ## id: the filter id. - ## Returns true if the filter was successfully uninstalled, otherwise false. - discard + if message.symKeyID.isSome(): + symKey = some(keys.symKeys[message.symKeyID.get().string]) - rpcsrv.rpc("shh_getFilterChanges") do(id: int) -> seq[WhisperMessage]: - ## Polling method for whisper filters. Returns new messages since the last call of this method. - ## Note: calling the shh_getMessages method, will reset the buffer for this method, so that you won't receive duplicate messages. - ## - ## id: the filter id. - discard + # Note: If no topic it will be defaulted to 0x00000000 + if message.topic.isSome(): + hexToByteArray(message.topic.get().string[2 .. ^1], topic) - rpcsrv.rpc("shh_getMessages") do(id: int) -> seq[WhisperMessage]: - ## Get all messages matching a filter. Unlike shh_getFilterChanges this returns all messages. - ## - ## id: the filter id. - ## Returns a list of messages received since last poll. - discard + if message.padding.isSome(): + padding = some(hexToSeqByte(message.padding.get().string)) + + if message.targetPeer.isSome(): + targetPeer = some(newNode(message.targetPeer.get()).id) + + result = node.postMessage(pubKey, + symKey, + sigPrivKey, + ttl = message.ttl.uint32, + topic = topic, + payload = hexToSeqByte(message.payload.string), + padding = padding, + powTime = message.powTime, + powTarget = message.powTarget, + targetPeer = targetPeer) diff --git a/tests/rpcclient/ethcallsigs.nim b/tests/rpcclient/ethcallsigs.nim index a34ed9fbb..f6d6ffbc4 100644 --- a/tests/rpcclient/ethcallsigs.nim +++ b/tests/rpcclient/ethcallsigs.nim @@ -75,3 +75,28 @@ proc shh_uninstallFilter(id: int): bool proc shh_getFilterChanges(id: int): seq[WhisperMessage] proc shh_getMessages(id: int): seq[WhisperMessage] ]# + +proc shh_version(): string +proc shh_info(): WhisperInfo +proc shh_setMaxMessageSize(size: uint64): bool +proc shh_setMinPoW(pow: float): bool +proc shh_markTrustedPeer(enode: string): bool + +proc shh_newKeyPair(): IdentifierStr +proc shh_addPrivateKey(key: string): IdentifierStr +proc shh_deleteKeyPair(id: IdentifierStr): bool +proc shh_hasKeyPair(id: IdentifierStr): bool +proc shh_getPublicKey(id: IdentifierStr): PublicKeyStr +proc shh_getPrivateKey(id: IdentifierStr): PrivateKeyStr + +proc shh_newSymKey(): IdentifierStr +proc shh_addSymKey(key: string): IdentifierStr +proc shh_generateSymKeyFromPassword(password: string): IdentifierStr +proc shh_hasSymKey(id: IdentifierStr): bool +proc shh_getSymKey(id: IdentifierStr): SymKeyStr +proc shh_deleteSymKey(id: IdentifierStr): bool + +proc shh_newMessageFilter(options: WhisperFilterOptions): IdentifierStr +proc shh_deleteMessageFilter(id: IdentifierStr): bool +proc shh_getFilterMessages(id: IdentifierStr): seq[WhisperFilterMessage] +proc shh_post(message: WhisperPostMessage): bool diff --git a/tests/test_rpc_whisper.nim b/tests/test_rpc_whisper.nim new file mode 100644 index 000000000..502ad5975 --- /dev/null +++ b/tests/test_rpc_whisper.nim @@ -0,0 +1,142 @@ +import + unittest, strformat, options, json_rpc/[rpcserver, rpcclient], + eth/common as eth_common, eth/p2p as eth_p2p, + eth/[rlp, keys], eth/p2p/rlpx_protocols/whisper_protocol, + ../nimbus/rpc/[common, hexstrings, rpc_types, whisper], ../nimbus/config + +from os import DirSep +from strutils import rsplit +template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0] + +## Generate client convenience marshalling wrappers from forward declarations +## For testing, ethcallsigs needs to be kept in sync with ../nimbus/rpc/[common, p2p] +const sigPath = &"{sourceDir}{DirSep}rpcclient{DirSep}ethcallsigs.nim" +createRpcSigs(RpcSocketClient, sigPath) + +proc setupEthNode: EthereumNode = + var + conf = getConfiguration() + keypair: KeyPair + keypair.seckey = conf.net.nodekey + keypair.pubkey = conf.net.nodekey.getPublicKey() + + var srvAddress: Address + srvAddress.ip = parseIpAddress("0.0.0.0") + srvAddress.tcpPort = Port(conf.net.bindPort) + srvAddress.udpPort = Port(conf.net.discPort) + result = newEthereumNode(keypair, srvAddress, conf.net.networkId, + nil, "nimbus 0.1.0", addAllCapabilities = false) + result.addCapability Whisper + +proc doTests = + var ethNode = setupEthNode() + + # Create Ethereum RPCs + let RPC_PORT = 8545 + var + rpcServer = newRpcSocketServer(["localhost:" & $RPC_PORT]) + client = newRpcSocketClient() + let keys = newWhisperKeys() + setupCommonRPC(rpcServer) + setupWhisperRPC(ethNode, keys, rpcServer) + + # Begin tests + rpcServer.start() + waitFor client.connect("localhost", Port(RPC_PORT)) + + suite "Whisper Remote Procedure Calls": + test "shh_version": + check waitFor(client.shh_version()) == whisperVersionStr + test "shh_info": + let info = waitFor client.shh_info() + check info.maxMessageSize == defaultMaxMsgSize + test "shh_setMaxMessageSize": + let testValue = 1024'u64 + check waitFor(client.shh_setMaxMessageSize(testValue)) == true + var info = waitFor client.shh_info() + check info.maxMessageSize == testValue + check waitFor(client.shh_setMaxMessageSize(defaultMaxMsgSize + 1)) == false + info = waitFor client.shh_info() + check info.maxMessageSize == testValue + test "shh_setMinPoW": + let testValue = 0.0001 + check waitFor(client.shh_setMinPoW(testValue)) == true + let info = waitFor client.shh_info() + check info.minPow == testValue + # test "shh_markTrustedPeer": + # TODO: need to connect a peer to test + test "shh asymKey tests": + let keyID = waitFor client.shh_newKeyPair() + check: + waitFor(client.shh_hasKeyPair(keyID)) == true + waitFor(client.shh_deleteKeyPair(keyID)) == true + waitFor(client.shh_hasKeyPair(keyID)) == false + waitFor(client.shh_deleteKeyPair(keyID)) == false + + let privkey = "0x5dc5381cae54ba3174dc0d46040fe11614d0cc94d41185922585198b4fcef9d3" + let pubkey = "0x04e5fd642a0f630bbb1e4cd7df629d7b8b019457a9a74f983c0484a045cebb176def86a54185b50bbba6bbf97779173695e92835d63109c23471e6da382f922fdb" + let keyID2 = waitFor client.shh_addPrivateKey(privkey) + check: + waitFor(client.shh_getPublicKey(keyID2)).string == pubkey + waitFor(client.shh_getPrivateKey(keyID2)).string == privkey + waitFor(client.shh_hasKeyPair(keyID2)) == true + waitFor(client.shh_deleteKeyPair(keyID2)) == true + waitFor(client.shh_hasKeyPair(keyID2)) == false + waitFor(client.shh_deleteKeyPair(keyID2)) == false + test "shh symKey tests": + let keyID = waitFor client.shh_newSymKey() + check: + waitFor(client.shh_hasSymKey(keyID)) == true + waitFor(client.shh_deleteSymKey(keyID)) == true + waitFor(client.shh_hasSymKey(keyID)) == false + waitFor(client.shh_deleteSymKey(keyID)) == false + + let symKey = "0x0000000000000000000000000000000000000000000000000000000000000001" + let keyID2 = waitFor client.shh_addSymKey(symKey) + check: + waitFor(client.shh_getSymKey(keyID2)).string == symKey + waitFor(client.shh_hasSymKey(keyID2)) == true + waitFor(client.shh_deleteSymKey(keyID2)) == true + waitFor(client.shh_hasSymKey(keyID2)) == false + waitFor(client.shh_deleteSymKey(keyID2)) == false + + test "shh symKey post and filter": + var options: WhisperFilterOptions + options.symKeyID = some(waitFor client.shh_newSymKey()) + options.topics = some(@["0x12345678".TopicStr]) + let filterID = waitFor client.shh_newMessageFilter(options) + + var message: WhisperPostMessage + message.symKeyID = options.symKeyID + message.ttl = 30 + message.topic = some("0x12345678".TopicStr) + message.payload = "0x45879632".HexDataStr + message.powTime = 1.0 + message.powTarget = 0.001 + check: + waitFor(client.shh_post(message)) == true + # TODO: this does not work due to overloads? + # var messages = waitFor client.shh_getFilterMessages(filterID) + + test "shh asymKey post and filter": + var options: WhisperFilterOptions + let keyID = waitFor client.shh_newKeyPair() + options.privateKeyID = some(keyID) + let filterID = waitFor client.shh_newMessageFilter(options) + + var message: WhisperPostMessage + message.pubKey = some(waitFor(client.shh_getPublicKey(keyID))) + message.ttl = 30 + message.topic = some("0x12345678".TopicStr) + message.payload = "0x45879632".HexDataStr + message.powTime = 1.0 + message.powTarget = 0.001 + check: + waitFor(client.shh_post(message)) == true + # TODO: this does not work due to overloads? + # var messages = waitFor client.shh_getFilterMessages(filterID) + + rpcServer.stop() + rpcServer.close() + +doTests()