diff --git a/Makefile b/Makefile index 0be828a89..4c9af9822 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,8 @@ DOCKER_IMAGE_NIM_PARAMS ?= -d:chronicles_colors:none -d:insecure deps \ update \ wakunode \ + wakusim \ + wakuexample \ test \ clean \ libbacktrace @@ -45,7 +47,7 @@ GIT_SUBMODULE_UPDATE := git submodule update --init --recursive else # "variables.mk" was included. Business as usual until the end of this file. # default target, because it's the first one that doesn't start with '.' -all: | wakunode wakusim wakunode2 wakusim2 +all: | wakunode wakusim wakuexample wakunode2 wakusim2 # must be included after the default target -include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk @@ -76,6 +78,10 @@ wakusim: | build deps wakunode echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim wakusim $(NIM_PARAMS) waku.nims +wakuexample: | build deps + echo -e $(BUILD_MSG) "build/$@" && \ + $(ENV_SCRIPT) nim wakuexample $(NIM_PARAMS) waku.nims + wakunode2: | build deps echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim wakunode2 $(NIM_PARAMS) waku.nims diff --git a/README.md b/README.md index af7867f74..7f1c9f6be 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ You can also run a specific test (and alter compile options as you want): nim c -r ./tests/v1/test_waku_connect.nim ``` +### Waku Protocol Example +There is a more basic example, more limited in features and configuration than +the `wakunode`, located in `examples/v1/example.nim`. + +More information on how to run this example can be found it its +[readme](examples/v1/README.md). + ### Waku Quick Simulation One can set up several nodes, get them connected and then instruct them via the JSON-RPC interface. This can be done via e.g. web3.js, nim-web3 (needs to be diff --git a/examples/v1/README.md b/examples/v1/README.md new file mode 100644 index 000000000..ef8c2799a --- /dev/null +++ b/examples/v1/README.md @@ -0,0 +1,42 @@ +# Waku v1 example +## Introduction +This is a basic Waku v1 example to show the Waku v1 API usage. + +It can be run as a single node, in which case it will just post and receive its +own messages. + +Or multiple nodes can be started and can connect to each other, so that +messages can be passed around. + +## How to build +```sh +make wakuexample +``` + +## How to run +### Single node +```sh +# Lauch example node +./build/example +``` + +Messages will be posted and received. + +### Multiple nodes + +```sh +# Launch first example node +./build/example +``` + +Now look for an `INFO` log containing the enode address, e.g.: +`enode://26..5b@0.0.0.0:30303` (but with full address) + +Copy the full enode string of the first node and start the second +node with that enode string as staticnode config option: +```sh +# Launch second example node, providing the enode address of the first node +./build/example --staticnode:enode://26..5b@0.0.0.0:30303 --ports-shift:1 +``` + +Now both nodes will receive also messages from each other. diff --git a/examples/v1/config_example.nim b/examples/v1/config_example.nim new file mode 100644 index 000000000..7ce2dede6 --- /dev/null +++ b/examples/v1/config_example.nim @@ -0,0 +1,65 @@ +import + confutils/defs, chronicles, chronos, eth/keys + +type + WakuNodeCmd* = enum + noCommand + + WakuNodeConf* = object + logLevel* {. + desc: "Sets the log level." + defaultValue: LogLevel.INFO + name: "log-level" .}: LogLevel + + case cmd* {. + command + defaultValue: noCommand .}: WakuNodeCmd + + of noCommand: + tcpPort* {. + desc: "TCP listening port." + defaultValue: 30303 + name: "tcp-port" .}: uint16 + + udpPort* {. + desc: "UDP listening port." + defaultValue: 30303 + name: "udp-port" .}: uint16 + + portsShift* {. + desc: "Add a shift to all port numbers." + defaultValue: 0 + name: "ports-shift" .}: uint16 + + nat* {. + desc: "Specify method to use for determining public address. " & + "Must be one of: any, none, upnp, pmp, extip:." + defaultValue: "any" .}: string + + staticnodes* {. + desc: "Enode URL to directly connect with. Argument may be repeated." + name: "staticnode" .}: seq[string] + + nodekey* {. + desc: "P2P node private key as hex.", + defaultValue: KeyPair.random(keys.newRng()[]) + name: "nodekey" .}: KeyPair + +proc parseCmdArg*(T: type KeyPair, p: TaintedString): T = + try: + let privkey = PrivateKey.fromHex(string(p)).tryGet() + result = privkey.toKeyPair() + except CatchableError as e: + raise newException(ConfigurationError, "Invalid private key") + +proc completeCmdArg*(T: type KeyPair, val: TaintedString): seq[string] = + return @[] + +proc parseCmdArg*(T: type IpAddress, p: TaintedString): T = + try: + result = parseIpAddress(p) + except CatchableError as e: + raise newException(ConfigurationError, "Invalid IP address") + +proc completeCmdArg*(T: type IpAddress, val: TaintedString): seq[string] = + return @[] diff --git a/examples/v1/example b/examples/v1/example new file mode 100755 index 000000000..f6d39ce04 Binary files /dev/null and b/examples/v1/example differ diff --git a/examples/v1/example.nim b/examples/v1/example.nim new file mode 100644 index 000000000..1ce0b8daa --- /dev/null +++ b/examples/v1/example.nim @@ -0,0 +1,91 @@ +import + confutils, chronicles, chronos, stew/byteutils, + eth/[keys, p2p, async_utils], + ../../waku/protocol/v1/waku_protocol, + ../../waku/node/v1/waku_helpers, + ./config_example + +## This is a simple Waku v1 example to show the Waku v1 API usage. + +const clientId = "Waku example v1" + +let + # Load the cli configuration from `config_example.nim`. + config = WakuNodeConf.load() + # Seed the rng. + rng = keys.newRng() + # Set up the address according to NAT information. + (ip, tcpPort, udpPort) = setupNat(config.nat, clientId, config.tcpPort, + config.udpPort, config.portsShift) + address = Address(ip: ip, tcpPort: tcpPort, udpPort: udpPort) + +# Create Ethereum Node +var node = newEthereumNode(config.nodekey, # Node identifier + address, # Address reachable for incoming requests + 1, # Network Id, only applicable for ETH protocol + nil, # Database, not required for Waku + clientId, # Client id string + addAllCapabilities = false, # Disable default all RLPx capabilities + rng = rng) + +node.addCapability Waku # Enable only the Waku protocol. + +# Set up the Waku configuration. +let wakuConfig = WakuConfig(powRequirement: 0.002, + bloom: some(fullBloom()), # Full bloom filter + isLightNode: false, # Full node + maxMsgSize: waku_protocol.defaultMaxMsgSize, + topics: none(seq[waku_protocol.Topic]) # empty topic interest + ) +node.configureWaku(wakuConfig) + +# Optionally direct connect to a set of nodes. +if config.staticnodes.len > 0: + connectToNodes(node, config.staticnodes) + +# Connect to the network, which will make the node start listening and/or +# connect to bootnodes, and/or start discovery. +# This will block until first connection is made, which in this case can only +# happen if we directly connect to nodes (step above) or if an incoming +# connection occurs, which is why we use `traceAsyncErrors` instead of `await`. +# TODO: This looks a bit awkward and the API should perhaps be altered here. +traceAsyncErrors node.connectToNetwork(@[], + true, # Enable listening + false # Disable discovery (only discovery v4 is currently supported) + ) + +# Using a hardcoded symmetric key for encryption of the payload for the sake of +# simplicity. +var symKey: SymKey +symKey[31] = 1 +# Asymmetric keypair to sign the payload. +let signKeyPair = KeyPair.random(rng[]) + +# Code to be executed on receival of a message on filter. +proc handler(msg: ReceivedMessage) = + if msg.decoded.src.isSome(): + echo "Received message from ", $msg.decoded.src.get(), ": ", + string.fromBytes(msg.decoded.payload) + +# Create and subscribe filter with above handler. +let + topic = [byte 0, 0, 0, 0] + filter = initFilter(symKey = some(symKey), topics = @[topic]) +discard node.subscribeFilter(filter, handler) + +# Repeat the posting of a message every 5 seconds. +proc repeatMessage(udata: pointer) {.gcsafe.} = + {.gcsafe.}: + # Post a waku message on the network, encrypted with provided symmetric key, + # signed with asymmetric key, on topic and with ttl of 30 seconds. + let posted = node.postMessage( + symKey = some(symKey), src = some(signKeyPair.seckey), + ttl = 30, topic = topic, payload = @[byte 0x48, 0x65, 0x6C, 0x6C, 0x6F]) + + if posted: echo "Posted message as ", $signKeyPair.pubkey + else: echo "Posting message failed." + + discard setTimer(Moment.fromNow(5.seconds), repeatMessage) +discard setTimer(Moment.fromNow(5.seconds), repeatMessage) + +runForever() diff --git a/waku.nimble b/waku.nimble index d226f3d8c..49a622205 100644 --- a/waku.nimble +++ b/waku.nimble @@ -43,13 +43,16 @@ task test, "Run waku v1 tests": task test2, "Run waku v2 tests": test "all_tests_v2" -task wakunode, "Build Waku cli": +task wakunode, "Build Waku v1 cli node": buildBinary "wakunode", "waku/node/v1/", "-d:chronicles_log_level=TRACE" -task wakusim, "Build Waku simulation tools": +task wakusim, "Build Waku v1 simulation tools": buildBinary "quicksim", "waku/node/v1/", "-d:chronicles_log_level=INFO" buildBinary "start_network", "waku/node/v1/", "-d:chronicles_log_level=DEBUG" +task wakuexample, "Build Waku v1 example": + buildBinary "example", "examples/v1/", "-d:chronicles_log_level=DEBUG" + # TODO Also build Waku store and filter protocols here task protocol2, "Build the experimental Waku protocol": buildBinary "waku_relay", "waku/protocol/v2/", "-d:chronicles_log_level=TRACE" diff --git a/waku/node/v1/waku_helpers.nim b/waku/node/v1/waku_helpers.nim new file mode 100644 index 000000000..aa23c3d81 --- /dev/null +++ b/waku/node/v1/waku_helpers.nim @@ -0,0 +1,55 @@ +import + std/strutils, + chronos, + eth/net/nat, eth/[p2p, async_utils], eth/p2p/peer_pool + +let globalListeningAddr = parseIpAddress("0.0.0.0") + +proc setBootNodes*(nodes: openArray[string]): seq[ENode] = + result = newSeqOfCap[ENode](nodes.len) + for nodeId in nodes: + # TODO: something more user friendly than an expect + result.add(ENode.fromString(nodeId).expect("correct node")) + +proc connectToNodes*(node: EthereumNode, nodes: openArray[string]) = + for nodeId in nodes: + # TODO: something more user friendly than an assert + let whisperENode = ENode.fromString(nodeId).expect("correct node") + + traceAsyncErrors node.peerPool.connectToNode(newNode(whisperENode)) + +proc setupNat*(natConf, clientId: string, tcpPort, udpPort, portsShift: uint16): + tuple[ip: IpAddress, tcpPort: Port, udpPort: Port] = + # defaults + result.ip = globalListeningAddr + result.tcpPort = Port(tcpPort + portsShift) + result.udpPort = Port(udpPort + portsShift) + + var nat: NatStrategy + case natConf.toLowerAscii(): + of "any": + nat = NatAny + of "none": + nat = NatNone + of "upnp": + nat = NatUpnp + of "pmp": + nat = NatPmp + else: + if natConf.startsWith("extip:") and isIpAddress(natConf[6..^1]): + # any required port redirection is assumed to be done by hand + result.ip = parseIpAddress(natConf[6..^1]) + nat = NatNone + else: + error "not a valid NAT mechanism, nor a valid IP address", value = natConf + quit(QuitFailure) + + if nat != NatNone: + let extIP = getExternalIP(nat) + if extIP.isSome: + result.ip = extIP.get() + let extPorts = redirectPorts(tcpPort = result.tcpPort, + udpPort = result.udpPort, + description = clientId) + if extPorts.isSome: + (result.tcpPort, result.udpPort) = extPorts.get() diff --git a/waku/node/v1/wakunode.nim b/waku/node/v1/wakunode.nim index b1c1aa933..20af59e1d 100644 --- a/waku/node/v1/wakunode.nim +++ b/waku/node/v1/wakunode.nim @@ -1,74 +1,23 @@ import - confutils, config, strutils, chronos, json_rpc/rpcserver, metrics, - metrics/chronicles_support, - eth/[keys, p2p, async_utils], eth/common/utils, eth/net/nat, + std/strutils, + confutils, chronos, json_rpc/rpcserver, metrics, metrics/chronicles_support, + eth/[keys, p2p, async_utils], eth/common/utils, eth/p2p/[discovery, enode, peer_pool, bootnodes, whispernodes], eth/p2p/rlpx_protocols/whisper_protocol, ../../protocol/v1/[waku_protocol, waku_bridge], - ./rpc/[waku, wakusim, key_storage] + ./rpc/[waku, wakusim, key_storage], ./waku_helpers, ./config const clientId = "Nimbus waku node" -let globalListeningAddr = parseIpAddress("0.0.0.0") - -proc setBootNodes(nodes: openArray[string]): seq[ENode] = - result = newSeqOfCap[ENode](nodes.len) - for nodeId in nodes: - # TODO: something more user friendly than an expect - result.add(ENode.fromString(nodeId).expect("correct node")) - -proc connectToNodes(node: EthereumNode, nodes: openArray[string]) = - for nodeId in nodes: - # TODO: something more user friendly than an assert - let whisperENode = ENode.fromString(nodeId).expect("correct node") - - traceAsyncErrors node.peerPool.connectToNode(newNode(whisperENode)) - -proc setupNat(conf: WakuNodeConf): tuple[ip: IpAddress, - tcpPort: Port, - udpPort: Port] = - # defaults - result.ip = globalListeningAddr - result.tcpPort = Port(conf.tcpPort + conf.portsShift) - result.udpPort = Port(conf.udpPort + conf.portsShift) - - var nat: NatStrategy - case conf.nat.toLowerAscii(): - of "any": - nat = NatAny - of "none": - nat = NatNone - of "upnp": - nat = NatUpnp - of "pmp": - nat = NatPmp - else: - if conf.nat.startsWith("extip:") and isIpAddress(conf.nat[6..^1]): - # any required port redirection is assumed to be done by hand - result.ip = parseIpAddress(conf.nat[6..^1]) - nat = NatNone - else: - error "not a valid NAT mechanism, nor a valid IP address", value = conf.nat - quit(QuitFailure) - - if nat != NatNone: - let extIP = getExternalIP(nat) - if extIP.isSome: - result.ip = extIP.get() - let extPorts = redirectPorts(tcpPort = result.tcpPort, - udpPort = result.udpPort, - description = clientId) - if extPorts.isSome: - (result.tcpPort, result.udpPort) = extPorts.get() - proc run(config: WakuNodeConf, rng: ref BrHmacDrbgContext) = let - (ip, tcpPort, udpPort) = setupNat(config) + (ip, tcpPort, udpPort) = setupNat(config.nat, clientId, config.tcpPort, + config.udpPort, config.portsShift) address = Address(ip: ip, tcpPort: tcpPort, udpPort: udpPort) # Set-up node var node = newEthereumNode(config.nodekey, address, 1, nil, clientId, - addAllCapabilities = false) + addAllCapabilities = false, rng = rng) if not config.bootnodeOnly: node.addCapability Waku # Always enable Waku protocol var topicInterest: Option[seq[waku_protocol.Topic]]