Move Nimbus Waku RPC code + add converted whisper rpc test

- Move Waku RPC code from Nimbus + necessary code around it
- Add Waku RPC test which is a copy of the Whisper RPC test
- Some renaming
- Remove nimbus submodule
This commit is contained in:
kdeme 2020-04-30 22:35:57 +02:00
parent fe1450d4b4
commit 87e4e5282a
15 changed files with 951 additions and 20 deletions

5
.gitmodules vendored
View File

@ -1,8 +1,3 @@
[submodule "vendor/nimbus"]
path = vendor/nimbus
url = https://github.com/status-im/nimbus.git
ignore = dirty
branch = master
[submodule "vendor/nim-eth"] [submodule "vendor/nim-eth"]
path = vendor/nim-eth path = vendor/nim-eth
url = https://github.com/status-im/nim-eth.git url = https://github.com/status-im/nim-eth.git

View File

@ -91,7 +91,7 @@ test: | build deps
# usual cleaning # usual cleaning
clean: | clean-common clean: | clean-common
rm -rf build/{wakunode,quicksim,start_network} rm -rf build/{wakunode,quicksim,start_network,all_tests}
ifneq ($(USE_LIBBACKTRACE), 0) ifneq ($(USE_LIBBACKTRACE), 0)
+ $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT) + $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT)
endif endif

2
tests/all_tests.nim Normal file
View File

@ -0,0 +1,2 @@
import
./test_rpc_waku

238
tests/test_rpc_waku.nim Normal file
View File

@ -0,0 +1,238 @@
import
unittest, strformat, options, stew/byteutils, json_rpc/[rpcserver, rpcclient],
eth/common as eth_common, eth/[rlp, keys, p2p],
eth/p2p/rlpx_protocols/waku_protocol,
../waku/node/v0/rpc/[hexstrings, rpc_types, waku, key_storage]
from os import DirSep, ParDir
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 ../waku/node/v0/rpc/waku
const sigPath = &"{sourceDir}{DirSep}{ParDir}{DirSep}waku{DirSep}node{DirSep}v0{DirSep}rpc{DirSep}wakucallsigs.nim"
createRpcSigs(RpcSocketClient, sigPath)
proc setupNode(capabilities: varargs[ProtocolInfo, `protocolInfo`]): EthereumNode =
let
keypair = KeyPair.random()[]
srvAddress = Address(ip: parseIpAddress("0.0.0.0"), tcpPort: Port(30303),
udpPort: Port(30303))
result = newEthereumNode(keypair, srvAddress, 1, nil, "waku test rpc",
addAllCapabilities = false)
for capability in capabilities:
result.addCapability capability
proc doTests {.async.} =
var ethNode = setupNode(Waku)
# Create Ethereum RPCs
let rpcPort = 8545
var
rpcServer = newRpcSocketServer(["localhost:" & $rpcPort])
client = newRpcSocketClient()
let keys = newKeyStorage()
setupWakuRPC(ethNode, keys, rpcServer)
# Begin tests
rpcServer.start()
await client.connect("localhost", Port(rpcPort))
suite "Waku Remote Procedure Calls":
test "waku_version":
check await(client.waku_version()) == wakuVersionStr
test "waku_info":
let info = await client.waku_info()
check info.maxMessageSize == defaultMaxMsgSize
test "waku_setMaxMessageSize":
let testValue = 1024'u64
check await(client.waku_setMaxMessageSize(testValue)) == true
var info = await client.waku_info()
check info.maxMessageSize == testValue
expect ValueError:
discard await(client.waku_setMaxMessageSize(defaultMaxMsgSize + 1))
info = await client.waku_info()
check info.maxMessageSize == testValue
test "waku_setMinPoW":
let testValue = 0.0001
check await(client.waku_setMinPoW(testValue)) == true
let info = await client.waku_info()
check info.minPow == testValue
# test "waku_markTrustedPeer":
# TODO: need to connect a peer to test
test "waku asymKey tests":
let keyID = await client.waku_newKeyPair()
check:
await(client.waku_hasKeyPair(keyID)) == true
await(client.waku_deleteKeyPair(keyID)) == true
await(client.waku_hasKeyPair(keyID)) == false
expect ValueError:
discard await(client.waku_deleteKeyPair(keyID))
let privkey = "0x5dc5381cae54ba3174dc0d46040fe11614d0cc94d41185922585198b4fcef9d3"
let pubkey = "0x04e5fd642a0f630bbb1e4cd7df629d7b8b019457a9a74f983c0484a045cebb176def86a54185b50bbba6bbf97779173695e92835d63109c23471e6da382f922fdb"
let keyID2 = await client.waku_addPrivateKey(privkey)
check:
await(client.waku_getPublicKey(keyID2)) == pubkey.toPublicKey
await(client.waku_getPrivateKey(keyID2)).toRaw() == privkey.toPrivateKey.toRaw()
await(client.waku_hasKeyPair(keyID2)) == true
await(client.waku_deleteKeyPair(keyID2)) == true
await(client.waku_hasKeyPair(keyID2)) == false
expect ValueError:
discard await(client.waku_deleteKeyPair(keyID2))
test "waku symKey tests":
let keyID = await client.waku_newSymKey()
check:
await(client.waku_hasSymKey(keyID)) == true
await(client.waku_deleteSymKey(keyID)) == true
await(client.waku_hasSymKey(keyID)) == false
expect ValueError:
discard await(client.waku_deleteSymKey(keyID))
let symKey = "0x0000000000000000000000000000000000000000000000000000000000000001"
let keyID2 = await client.waku_addSymKey(symKey)
check:
await(client.waku_getSymKey(keyID2)) == symKey.toSymKey
await(client.waku_hasSymKey(keyID2)) == true
await(client.waku_deleteSymKey(keyID2)) == true
await(client.waku_hasSymKey(keyID2)) == false
expect ValueError:
discard await(client.waku_deleteSymKey(keyID2))
let keyID3 = await client.waku_generateSymKeyFromPassword("password")
let keyID4 = await client.waku_generateSymKeyFromPassword("password")
let keyID5 = await client.waku_generateSymKeyFromPassword("nimbus!")
check:
await(client.waku_getSymKey(keyID3)) ==
await(client.waku_getSymKey(keyID4))
await(client.waku_getSymKey(keyID3)) !=
await(client.waku_getSymKey(keyID5))
await(client.waku_hasSymKey(keyID3)) == true
await(client.waku_deleteSymKey(keyID3)) == true
await(client.waku_hasSymKey(keyID3)) == false
expect ValueError:
discard await(client.waku_deleteSymKey(keyID3))
# Some defaults for the filter & post tests
let
ttl = 30'u64
topicStr = "0x12345678"
payload = "0x45879632"
# A very low target and long time so we are sure the test never fails
# because of this
powTarget = 0.001
powTime = 1.0
test "waku filter create and delete":
let
topic = topicStr.toTopic()
symKeyID = await client.waku_newSymKey()
options = WakuFilterOptions(symKeyID: some(symKeyID),
topics: some(@[topic]))
filterID = await client.waku_newMessageFilter(options)
check:
filterID.string.isValidIdentifier
await(client.waku_deleteMessageFilter(filterID)) == true
expect ValueError:
discard await(client.waku_deleteMessageFilter(filterID))
test "waku symKey post and filter loop":
let
topic = topicStr.toTopic()
symKeyID = await client.waku_newSymKey()
options = WakuFilterOptions(symKeyID: some(symKeyID),
topics: some(@[topic]))
filterID = await client.waku_newMessageFilter(options)
message = WakuPostMessage(symKeyID: some(symKeyID),
ttl: ttl,
topic: some(topic),
payload: payload.HexDataStr,
powTime: powTime,
powTarget: powTarget)
check:
await(client.waku_setMinPoW(powTarget)) == true
await(client.waku_post(message)) == true
let messages = await client.waku_getFilterMessages(filterID)
check:
messages.len == 1
messages[0].sig.isNone()
messages[0].recipientPublicKey.isNone()
messages[0].ttl == ttl
messages[0].topic == topic
messages[0].payload == hexToSeqByte(payload)
messages[0].padding.len > 0
messages[0].pow >= powTarget
await(client.waku_deleteMessageFilter(filterID)) == true
test "waku asymKey post and filter loop":
let
topic = topicStr.toTopic()
privateKeyID = await client.waku_newKeyPair()
options = WakuFilterOptions(privateKeyID: some(privateKeyID))
filterID = await client.waku_newMessageFilter(options)
pubKey = await client.waku_getPublicKey(privateKeyID)
message = WakuPostMessage(pubKey: some(pubKey),
ttl: ttl,
topic: some(topic),
payload: payload.HexDataStr,
powTime: powTime,
powTarget: powTarget)
check:
await(client.waku_setMinPoW(powTarget)) == true
await(client.waku_post(message)) == true
let messages = await client.waku_getFilterMessages(filterID)
check:
messages.len == 1
messages[0].sig.isNone()
messages[0].recipientPublicKey.get() == pubKey
messages[0].ttl == ttl
messages[0].topic == topic
messages[0].payload == hexToSeqByte(payload)
messages[0].padding.len > 0
messages[0].pow >= powTarget
await(client.waku_deleteMessageFilter(filterID)) == true
test "waku signature in post and filter loop":
let
topic = topicStr.toTopic()
symKeyID = await client.waku_newSymKey()
privateKeyID = await client.waku_newKeyPair()
pubKey = await client.waku_getPublicKey(privateKeyID)
options = WakuFilterOptions(symKeyID: some(symKeyID),
topics: some(@[topic]),
sig: some(pubKey))
filterID = await client.waku_newMessageFilter(options)
message = WakuPostMessage(symKeyID: some(symKeyID),
sig: some(privateKeyID),
ttl: ttl,
topic: some(topic),
payload: payload.HexDataStr,
powTime: powTime,
powTarget: powTarget)
check:
await(client.waku_setMinPoW(powTarget)) == true
await(client.waku_post(message)) == true
let messages = await client.waku_getFilterMessages(filterID)
check:
messages.len == 1
messages[0].sig.get() == pubKey
messages[0].recipientPublicKey.isNone()
messages[0].ttl == ttl
messages[0].topic == topic
messages[0].payload == hexToSeqByte(payload)
messages[0].padding.len > 0
messages[0].pow >= powTarget
await(client.waku_deleteMessageFilter(filterID)) == true
rpcServer.stop()
rpcServer.close()
waitFor doTests()

1
vendor/nimbus vendored

@ -1 +0,0 @@
Subproject commit ff028982d6fc414c17d98d4c5e6d6d3856c0c598

View File

@ -38,8 +38,7 @@ proc test(name: string, lang = "c") =
### Tasks ### Tasks
task test, "Run tests": task test, "Run tests":
discard test "all_tests"
# test "all_tests"
task wakunode, "Build Waku cli": task wakunode, "Build Waku cli":
buildBinary "wakunode", "waku/node/v0/", "-d:chronicles_log_level=TRACE" buildBinary "wakunode", "waku/node/v0/", "-d:chronicles_log_level=TRACE"

View File

@ -1,6 +1,5 @@
import import
confutils/defs, chronicles, chronos, confutils/defs, chronicles, chronos, eth/keys
eth/keys, eth/p2p/rlpx_protocols/waku_protocol
type type
Fleet* = enum Fleet* = enum

View File

@ -1,7 +1,7 @@
import import
os, strformat, chronicles, json_rpc/[rpcclient, rpcserver], nimcrypto/sysrand, os, strformat, chronicles, json_rpc/[rpcclient, rpcserver], nimcrypto/sysrand,
eth/common as eth_common, eth/keys, eth/p2p/rlpx_protocols/waku_protocol, eth/common as eth_common, eth/keys, eth/p2p/rlpx_protocols/waku_protocol,
../../vendor/nimbus/nimbus/rpc/[hexstrings, rpc_types, waku], ./rpc/[hexstrings, rpc_types],
options as what # TODO: Huh? Redefinition? options as what # TODO: Huh? Redefinition?
from os import DirSep from os import DirSep
@ -33,18 +33,18 @@ let
symKey = "0x0000000000000000000000000000000000000000000000000000000000000001" symKey = "0x0000000000000000000000000000000000000000000000000000000000000001"
topics = generateTopics() topics = generateTopics()
symKeyID = waitFor lightNode.waku_addSymKey(symKey) symKeyID = waitFor lightNode.waku_addSymKey(symKey)
options = WhisperFilterOptions(symKeyID: some(symKeyID), options = WakuFilterOptions(symKeyID: some(symKeyID),
topics: some(topics)) topics: some(topics))
filterID = waitFor lightNode.waku_newMessageFilter(options) filterID = waitFor lightNode.waku_newMessageFilter(options)
symKeyID2 = waitFor lightNode2.waku_addSymKey(symKey) symKeyID2 = waitFor lightNode2.waku_addSymKey(symKey)
options2 = WhisperFilterOptions(symKeyID: some(symKeyID2), options2 = WakuFilterOptions(symKeyID: some(symKeyID2),
topics: some(topics)) topics: some(topics))
filterID2 = waitFor lightNode2.waku_newMessageFilter(options2) filterID2 = waitFor lightNode2.waku_newMessageFilter(options2)
symkeyID3 = waitFor trafficNode.waku_addSymKey(symKey) symkeyID3 = waitFor trafficNode.waku_addSymKey(symKey)
var message = WhisperPostMessage(symKeyID: some(symkeyID3), var message = WakuPostMessage(symKeyID: some(symkeyID3),
ttl: 30, ttl: 30,
topic: some(topics[0]), topic: some(topics[0]),
payload: "0x45879632".HexDataStr, payload: "0x45879632".HexDataStr,

View File

@ -0,0 +1,225 @@
# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
## This module implements the Ethereum hexadecimal string formats for JSON
## See: https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding
#[
Note:
The following types are converted to hex strings when marshalled to JSON:
* Hash256
* UInt256
* seq[byte]
* openArray[seq]
* PublicKey
* PrivateKey
* SymKey
* Topic
* Bytes
]#
import
stint, stew/byteutils, eth/[keys, rlp], eth/common/eth_types,
eth/p2p/rlpx_protocols/waku_protocol
type
HexDataStr* = distinct string
Identifier* = distinct string # 32 bytes, no 0x prefix!
HexStrings = HexDataStr | Identifier
# Hex validation
template hasHexHeader(value: string): bool =
if value.len >= 2 and value[0] == '0' and value[1] in {'x', 'X'}: true
else: false
template isHexChar(c: char): bool =
if c notin {'0'..'9'} and
c notin {'a'..'f'} and
c notin {'A'..'F'}: false
else: true
func isValidHexQuantity*(value: string): bool =
if not value.hasHexHeader:
return false
# No leading zeros (but allow 0x0)
if value.len < 3 or (value.len > 3 and value[2] == '0'): return false
for i in 2 ..< value.len:
let c = value[i]
if not c.isHexChar:
return false
return true
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
# Leading zeros are allowed
for i in 2 ..< value.len:
let c = value[i]
if not c.isHexChar:
return false
return true
template isValidHexData(value: string, hexLen: int, header = true): bool =
value.len == hexLen and value.isValidHexData(header)
func isValidIdentifier*(value: string): bool =
# 32 bytes for Whisper ID, no 0x prefix
result = value.isValidHexData(64, false)
func isValidPublicKey*(value: string): bool =
# 65 bytes for Public Key plus 1 byte for 0x prefix
result = value.isValidHexData(132)
func isValidPrivateKey*(value: string): bool =
# 32 bytes for Private Key plus 1 byte for 0x prefix
result = value.isValidHexData(66)
func isValidSymKey*(value: string): bool =
# 32 bytes for Private Key plus 1 byte for 0x prefix
result = value.isValidHexData(66)
func isValidHash256*(value: string): bool =
# 32 bytes for Hash256 plus 1 byte for 0x prefix
result = value.isValidHexData(66)
func isValidTopic*(value: string): bool =
# 4 bytes for Topic plus 1 byte for 0x prefix
result = value.isValidHexData(10)
const
SInvalidData = "Invalid hex data format for Ethereum"
proc validateHexData*(value: string) {.inline.} =
if unlikely(not value.isValidHexData):
raise newException(ValueError, SInvalidData & ": " & value)
# Initialisation
proc hexDataStr*(value: string): HexDataStr {.inline.} =
value.validateHexData
result = value.HexDataStr
# Converters for use in RPC
import json
from json_rpc/rpcserver import expect
proc `%`*(value: HexStrings): JsonNode =
result = %(value.string)
# Overloads to support expected representation of hex data
proc `%`*(value: Hash256): JsonNode =
#result = %("0x" & $value) # More clean but no lowercase :(
result = %("0x" & value.data.toHex)
proc `%`*(value: UInt256): JsonNode =
result = %("0x" & value.toString(16))
proc `%`*(value: PublicKey): JsonNode =
result = %("0x04" & $value)
proc `%`*(value: PrivateKey): JsonNode =
result = %("0x" & $value)
proc `%`*(value: SymKey): JsonNode =
result = %("0x" & value.toHex)
proc `%`*(value: waku_protocol.Topic): JsonNode =
result = %("0x" & value.toHex)
proc `%`*(value: seq[byte]): JsonNode =
result = %("0x" & value.toHex)
# Helpers for the fromJson procs
proc toPublicKey*(key: string): PublicKey {.inline.} =
result = PublicKey.fromHex(key[4 .. ^1]).tryGet()
proc toPrivateKey*(key: string): PrivateKey {.inline.} =
result = PrivateKey.fromHex(key[2 .. ^1]).tryGet()
proc toSymKey*(key: string): SymKey {.inline.} =
hexToByteArray(key[2 .. ^1], result)
proc toTopic*(topic: string): waku_protocol.Topic {.inline.} =
hexToByteArray(topic[2 .. ^1], result)
# Marshalling from JSON to Nim types that includes format checking
func invalidMsg(name: string): string = "When marshalling from JSON, parameter \"" & name & "\" is not valid"
proc fromJson*(n: JsonNode, argName: string, result: var HexDataStr) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHexData:
raise newException(ValueError, invalidMsg(argName) & " as Ethereum data \"" & hexStr & "\"")
result = hexStr.hexDataStr
proc fromJson*(n: JsonNode, argName: string, result: var Identifier) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidIdentifier:
raise newException(ValueError, invalidMsg(argName) & " as a identifier \"" & hexStr & "\"")
result = hexStr.Identifier
proc fromJson*(n: JsonNode, argName: string, result: var UInt256) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not (hexStr.len <= 66 and hexStr.isValidHexQuantity):
raise newException(ValueError, invalidMsg(argName) & " as a UInt256 \"" & hexStr & "\"")
result = readUintBE[256](hexToPaddedByteArray[32](hexStr))
proc fromJson*(n: JsonNode, argName: string, result: var PublicKey) =
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.toPublicKey
proc fromJson*(n: JsonNode, argName: string, result: var PrivateKey) =
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.toPrivateKey
proc fromJson*(n: JsonNode, argName: string, result: var SymKey) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidSymKey:
raise newException(ValueError, invalidMsg(argName) & " as a symmetric key \"" & hexStr & "\"")
result = toSymKey(hexStr)
proc fromJson*(n: JsonNode, argName: string, result: var waku_protocol.Topic) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidTopic:
raise newException(ValueError, invalidMsg(argName) & " as a topic \"" & hexStr & "\"")
result = toTopic(hexStr)
# Following procs currently required only for testing, the `createRpcSigs` macro
# requires it as it will convert the JSON results back to the original Nim
# types, but it needs the `fromJson` calls for those specific Nim types to do so
proc fromJson*(n: JsonNode, argName: string, result: var seq[byte]) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHexData:
raise newException(ValueError, invalidMsg(argName) & " as a hex data \"" & hexStr & "\"")
result = hexToSeqByte(hexStr)
proc fromJson*(n: JsonNode, argName: string, result: var Hash256) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHash256:
raise newException(ValueError, invalidMsg(argName) & " as a Hash256 \"" & hexStr & "\"")
hexToByteArray(hexStr, result.data)

View File

@ -0,0 +1,22 @@
#
# Nimbus
# (c) Copyright 2019
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import tables, eth/keys, eth/p2p/rlpx_protocols/whisper/whisper_types
type
KeyStorage* = ref object
asymKeys*: Table[string, KeyPair]
symKeys*: Table[string, SymKey]
KeyGenerationError* = object of CatchableError
proc newKeyStorage*(): KeyStorage =
new(result)
result.asymKeys = initTable[string, KeyPair]()
result.symKeys = initTable[string, SymKey]()

View File

@ -0,0 +1,58 @@
import
hexstrings, options, eth/[keys, rlp],
eth/p2p/rlpx_protocols/waku_protocol
#[
Notes:
* Some of the types suppose 'null' when there is no appropriate value.
To allow for this, you can use Option[T] or use refs so the JSON transform can convert to `JNull`.
* Parameter objects from users must have their data verified so will use EthAddressStr instead of EthAddres, for example
* Objects returned to the user can use native Waku types, where hexstrings provides converters to hex strings.
This is because returned arrays in JSON is
a) not an efficient use of space
b) not the format the user expects (for example addresses are expected to be hex strings prefixed by "0x")
]#
type
WakuInfo* = object
# Returned to user
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.
WakuFilterOptions* = object
# Parameter from user
symKeyID*: Option[Identifier] # ID of symmetric key for message decryption.
privateKeyID*: Option[Identifier] # ID of private (asymmetric) key for message decryption.
sig*: Option[PublicKey] # (Optional) Public key of the signature.
minPow*: Option[float64] # (Optional) Minimal PoW requirement for incoming messages.
topics*: Option[seq[waku_protocol.Topic]] # (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.
WakuFilterMessage* = 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*: waku_protocol.Topic # 4 Bytes: Message topic.
payload*: seq[byte] # Decrypted payload.
padding*: seq[byte] # (Optional) Padding (byte array of arbitrary length).
pow*: float64 # Proof of work value.
hash*: Hash # Hash of the enveloped message.
WakuPostMessage* = object
# Parameter from user
symKeyID*: Option[Identifier] # ID of symmetric key for message encryption.
pubKey*: Option[PublicKey] # Public key for message encryption.
sig*: Option[Identifier] # (Optional) ID of the signing key.
ttl*: uint64 # Time-to-live in seconds.
topic*: Option[waku_protocol.Topic] # 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).

363
waku/node/v0/rpc/waku.nim Normal file
View File

@ -0,0 +1,363 @@
import
json_rpc/rpcserver, tables, options, sequtils,
eth/[common, rlp, keys, p2p], eth/p2p/rlpx_protocols/waku_protocol,
nimcrypto/[sysrand, hmac, sha2, pbkdf2],
rpc_types, hexstrings, key_storage
from stew/byteutils import hexToSeqByte, hexToByteArray
# Blatant copy of Whisper RPC but for the Waku protocol
proc setupWakuRPC*(node: EthereumNode, keys: KeyStorage, rpcsrv: RpcServer) =
rpcsrv.rpc("waku_version") do() -> string:
## Returns string of the current Waku protocol version.
result = wakuVersionStr
rpcsrv.rpc("waku_info") do() -> WakuInfo:
## Returns diagnostic information about the Waku node.
let config = node.protocolState(Waku).config
result = WakuInfo(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("waku_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.
## Waku 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)
if not result:
raise newException(ValueError, "Invalid size")
rpcsrv.rpc("waku_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.
# Note: `setPowRequirement` does not raise on failures of sending the update
# to the peers. Hence in theory this should not causes errors.
await node.setPowRequirement(pow)
result = true
# TODO: change string in to ENodeStr with extra checks
rpcsrv.rpc("waku_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)
if not result:
raise newException(ValueError, "Not a peer")
rpcsrv.rpc("waku_newKeyPair") do() -> Identifier:
## 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().Identifier
keys.asymKeys.add(result.string, KeyPair.random().tryGet())
rpcsrv.rpc("waku_addPrivateKey") do(key: PrivateKey) -> Identifier:
## 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().Identifier
keys.asymKeys.add(result.string, key.toKeyPair().tryGet())
rpcsrv.rpc("waku_deleteKeyPair") do(id: Identifier) -> 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)
if not result:
raise newException(ValueError, "Invalid key id")
rpcsrv.rpc("waku_hasKeyPair") do(id: Identifier) -> bool:
## Checks if the Waku node has a private key of a key pair matching the
## given ID.
##
## id: Identifier of key pair
##
## Returns (true or false) on success and an error on failure.
result = keys.asymkeys.hasKey(id.string)
rpcsrv.rpc("waku_getPublicKey") do(id: Identifier) -> 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("waku_getPrivateKey") do(id: Identifier) -> 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("waku_newSymKey") do() -> Identifier:
## 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().Identifier
var key: SymKey
if randomBytes(key) != key.len:
raise newException(KeyGenerationError, "Failed generating key")
keys.symKeys.add(result.string, key)
rpcsrv.rpc("waku_addSymKey") do(key: SymKey) -> Identifier:
## 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().Identifier
keys.symKeys.add(result.string, key)
rpcsrv.rpc("waku_generateSymKeyFromPassword") do(password: string) -> Identifier:
## Generates the key from password, stores it, and returns its ID.
##
## password: Password.
##
## Returns key identifier on success and an error on failure.
## Warning: an empty string is used as salt because the shh RPC API does not
## allow for passing a salt. A very good password is necessary (calculate
## yourself what that means :))
var ctx: HMAC[sha256]
var symKey: SymKey
if pbkdf2(ctx, password, "", 65356, symKey) != sizeof(SymKey):
raise newException(KeyGenerationError, "Failed generating key")
result = generateRandomID().Identifier
keys.symKeys.add(result.string, symKey)
rpcsrv.rpc("waku_hasSymKey") do(id: Identifier) -> 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("waku_getSymKey") do(id: Identifier) -> 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("waku_deleteSymKey") do(id: Identifier) -> 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)
if not result:
raise newException(ValueError, "Invalid key id")
rpcsrv.rpc("waku_subscribe") do(id: string,
options: WakuFilterOptions) -> Identifier:
## Creates and registers a new subscription to receive notifications for
## inbound Waku messages. Returns the ID of the newly created
## subscription.
##
## id: identifier of function call. In case of Waku must contain the
## value "messages".
## options: WakuFilterOptions
##
## Returns the subscription ID on success, the error on failure.
# TODO: implement subscriptions, only for WS & IPC?
discard
rpcsrv.rpc("waku_unsubscribe") do(id: Identifier) -> bool:
## Cancels and removes an existing subscription.
##
## id: Subscription identifier
##
## Returns true on success, the error on failure
result = node.unsubscribeFilter(id.string)
if not result:
raise newException(ValueError, "Invalid filter id")
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("waku_newMessageFilter") do(options: WakuFilterOptions) -> Identifier:
## Create a new filter within the node. This filter can be used to poll for
## new messages that match the set of criteria.
##
## options: WakuFilterOptions
##
## 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
src: Option[PublicKey]
privateKey: Option[PrivateKey]
symKey: Option[SymKey]
topics: seq[waku_protocol.Topic]
powReq: float64
allowP2P: bool
src = options.sig
if options.privateKeyID.isSome():
privateKey = some(keys.asymKeys[options.privateKeyID.get().string].seckey)
if options.symKeyID.isSome():
symKey= some(keys.symKeys[options.symKeyID.get().string])
if options.minPow.isSome():
powReq = options.minPow.get()
if options.topics.isSome():
topics = options.topics.get()
if options.allowP2P.isSome():
allowP2P = options.allowP2P.get()
let filter = initFilter(src, privateKey, symKey, topics, powReq, allowP2P)
result = node.subscribeFilter(filter).Identifier
# TODO: Should we do this here "automatically" or separate it in another
# RPC call? Is there a use case for that?
# Same could be said about bloomfilter, except that there is a use case
# there to have a full node no matter what message filters.
# Could also be moved to waku_protocol.nim
let config = node.protocolState(Waku).config
if config.topics.isSome():
try:
# TODO: an addTopics call would probably be more useful
let result = await node.setTopicInterest(config.topics.get().concat(filter.topics))
if not result:
raise newException(ValueError, "Too many topics")
except CatchableError:
trace "setTopics error occured"
elif config.isLightNode:
try:
await node.setBloomFilter(node.filtersToBloom())
except CatchableError:
trace "setBloomFilter error occured"
rpcsrv.rpc("waku_deleteMessageFilter") do(id: Identifier) -> 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)
if not result:
raise newException(ValueError, "Invalid filter id")
rpcsrv.rpc("waku_getFilterMessages") do(id: Identifier) -> seq[WakuFilterMessage]:
## 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 `waku_newMessageFilter`.
##
## Returns array of messages on success and an error on failure.
let messages = node.getFilterMessages(id.string)
for msg in messages:
result.add WakuFilterMessage(
sig: msg.decoded.src,
recipientPublicKey: msg.dst,
ttl: msg.ttl,
topic: msg.topic,
timestamp: msg.timestamp,
payload: msg.decoded.payload,
# Note: waku_protocol padding is an Option as there is the
# possibility of 0 padding in case of custom padding.
padding: msg.decoded.padding.get(@[]),
pow: msg.pow,
hash: msg.hash)
rpcsrv.rpc("waku_post") do(message: WakuPostMessage) -> bool:
## Creates a Waku message and injects it into the network for
## distribution.
##
## message: Waku message to post.
##
## Returns true on success and an error on failure.
# 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)
var
sigPrivKey: Option[PrivateKey]
symKey: Option[SymKey]
topic: waku_protocol.Topic
padding: Option[seq[byte]]
targetPeer: Option[NodeId]
if message.sig.isSome():
sigPrivKey = some(keys.asymKeys[message.sig.get().string].seckey)
if message.symKeyID.isSome():
symKey = some(keys.symKeys[message.symKeyID.get().string])
# Note: If no topic it will be defaulted to 0x00000000
if message.topic.isSome():
topic = message.topic.get()
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(message.pubKey,
symKey,
sigPrivKey,
ttl = message.ttl.uint32,
topic = topic,
payload = hexToSeqByte(message.payload.string),
padding = padding,
powTime = message.powTime,
powTarget = message.powTarget,
targetPeer = targetPeer)
if not result:
raise newException(ValueError, "Message could not be posted")

View File

@ -1,5 +1,5 @@
proc waku_version(): string proc waku_version(): string
proc waku_info(): WhisperInfo proc waku_info(): WakuInfo
proc waku_setMaxMessageSize(size: uint64): bool proc waku_setMaxMessageSize(size: uint64): bool
proc waku_setMinPoW(pow: float): bool proc waku_setMinPoW(pow: float): bool
proc waku_markTrustedPeer(enode: string): bool proc waku_markTrustedPeer(enode: string): bool
@ -18,10 +18,10 @@ proc waku_hasSymKey(id: Identifier): bool
proc waku_getSymKey(id: Identifier): SymKey proc waku_getSymKey(id: Identifier): SymKey
proc waku_deleteSymKey(id: Identifier): bool proc waku_deleteSymKey(id: Identifier): bool
proc waku_newMessageFilter(options: WhisperFilterOptions): Identifier proc waku_newMessageFilter(options: WakuFilterOptions): Identifier
proc waku_deleteMessageFilter(id: Identifier): bool proc waku_deleteMessageFilter(id: Identifier): bool
proc waku_getFilterMessages(id: Identifier): seq[WhisperFilterMessage] proc waku_getFilterMessages(id: Identifier): seq[WakuFilterMessage]
proc waku_post(message: WhisperPostMessage): bool proc waku_post(message: WakuPostMessage): bool
proc wakusim_generateTraffic(amount: int): bool proc wakusim_generateTraffic(amount: int): bool
proc wakusim_generateRandomTraffic(amount: int): bool proc wakusim_generateRandomTraffic(amount: int): bool

View File

@ -0,0 +1,31 @@
import
json_rpc/rpcserver, stew/endians2, nimcrypto/sysrand,
eth/[p2p, async_utils], eth/p2p/rlpx_protocols/waku_protocol
proc generateTraffic(node: EthereumNode, amount = 100) {.async.} =
var topicNumber = 0'u32
let payload = @[byte 0]
for i in 0..<amount:
discard waku_protocol.postMessage(node, ttl = 10,
topic = toBytesLE(i.uint32), payload = payload)
await sleepAsync(1.milliseconds)
proc generateRandomTraffic(node: EthereumNode, amount = 100) {.async.} =
var topic: array[4, byte]
let payload = @[byte 0]
for i in 0..<amount:
while randomBytes(topic) != 4:
discard
discard waku_protocol.postMessage(node, ttl = 10, topic = topic,
payload = payload)
await sleepAsync(1.milliseconds)
proc setupWakuSimRPC*(node: EthereumNode, rpcsrv: RpcServer) =
rpcsrv.rpc("wakusim_generateTraffic") do(amount: int) -> bool:
traceAsyncErrors node.generateTraffic(amount)
return true
rpcsrv.rpc("wakusim_generateRandomTraffic") do(amount: int) -> bool:
traceAsyncErrors node.generateRandomTraffic(amount)
return true

View File

@ -4,7 +4,7 @@ import
eth/[keys, p2p, async_utils], eth/common/utils, eth/net/nat, eth/[keys, p2p, async_utils], eth/common/utils, eth/net/nat,
eth/p2p/[discovery, enode, peer_pool, bootnodes, whispernodes], eth/p2p/[discovery, enode, peer_pool, bootnodes, whispernodes],
eth/p2p/rlpx_protocols/[whisper_protocol, waku_protocol, waku_bridge], eth/p2p/rlpx_protocols/[whisper_protocol, waku_protocol, waku_bridge],
../../vendor/nimbus/nimbus/rpc/[waku, wakusim, key_storage] ./rpc/[waku, wakusim, key_storage]
const clientId = "Nimbus waku node" const clientId = "Nimbus waku node"