Initial implementation of Whisper RPC

This commit is contained in:
kdeme 2019-03-23 21:54:28 +01:00
parent 29a226da1e
commit d43f20c65a
6 changed files with 632 additions and 111 deletions

View File

@ -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)

View File

@ -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

View File

@ -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).

View File

@ -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)

View File

@ -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

142
tests/test_rpc_whisper.nim Normal file
View File

@ -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()