mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-12 13:24:21 +00:00
Initial implementation of Whisper RPC
This commit is contained in:
parent
29a226da1e
commit
d43f20c65a
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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)
|
||||
|
@ -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
142
tests/test_rpc_whisper.nim
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user