diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f98f73d --- /dev/null +++ b/examples/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 example1 +``` + +## 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/config_example.nim b/examples/config_example.nim new file mode 100644 index 0000000..bbcb5c4 --- /dev/null +++ b/examples/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: string): 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: string): seq[string] = + return @[] + +proc parseCmdArg*(T: type IpAddress, p: string): T = + try: + result = parseIpAddress(p) + except CatchableError as e: + raise newException(ConfigurationError, "Invalid IP address") + +proc completeCmdArg*(T: type IpAddress, val: string): seq[string] = + return @[] diff --git a/examples/example.nim b/examples/example.nim new file mode 100644 index 0000000..76bf725 --- /dev/null +++ b/examples/example.nim @@ -0,0 +1,119 @@ +import + confutils, chronicles, chronos, stew/byteutils, stew/shims/net as stewNet, + eth/[keys, p2p], + ../waku/protocol/waku_protocol, + ../waku/node/waku_helpers, + ../waku/common/utils/nat, + ./config_example + +## This is a simple Waku v1 example to show the Waku v1 API usage. + +const clientId = "Waku example v1" + +proc run(config: WakuNodeConf, rng: ref HmacDrbgContext) = + + let natRes = setupNat(config.nat, clientId, + Port(config.tcpPort + config.portsShift), + Port(config.udpPort + config.portsShift)) + if natRes.isErr(): + fatal "setupNat failed", error = natRes.error + quit(1) + + # Set up the address according to NAT information. + let (ipExt, tcpPortExt, udpPortExt) = natRes.get() + # TODO: EthereumNode should have a better split of binding address and + # external address. Also, can't have different ports as it stands now. + let address = if ipExt.isNone(): + Address(ip: parseIpAddress("0.0.0.0"), + tcpPort: Port(config.tcpPort + config.portsShift), + udpPort: Port(config.udpPort + config.portsShift)) + else: + Address(ip: ipExt.get(), + tcpPort: Port(config.tcpPort + config.portsShift), + udpPort: Port(config.udpPort + config.portsShift)) + + # Create Ethereum Node + var node = newEthereumNode(config.nodekey, # Node identifier + address, # Address reachable for incoming requests + NetworkId(1), # Network Id, only applicable for ETH protocol + clientId, # Client id string + addAllCapabilities = false, # Disable default all RLPx capabilities + bindUdpPort = address.udpPort, # Assume same as external + bindTcpPort = address.tcpPort, # Assume same as external + 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 a callback to exit on errors instead of + # using `await`. + # TODO: This looks a bit awkward and the API should perhaps be altered here. + let connectedFut = node.connectToNetwork( + true, # Enable listening + false # Disable discovery (only discovery v4 is currently supported) + ) + connectedFut.callback = proc(data: pointer) {.gcsafe.} = + {.gcsafe.}: + if connectedFut.failed: + fatal "connectToNetwork failed", msg = connectedFut.readError.msg + quit(1) + + # 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. + # https://github.com/nim-lang/Nim/issues/17369 + var repeatMessage: proc(udata: pointer) {.gcsafe, raises: [Defect].} + repeatMessage = proc(udata: pointer) = + {.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() + +when isMainModule: + let + rng = keys.newRng() + conf = WakuNodeConf.load() + run(conf, rng) diff --git a/examples/nim.cfg b/examples/nim.cfg new file mode 100644 index 0000000..fecbc9f --- /dev/null +++ b/examples/nim.cfg @@ -0,0 +1,2 @@ +-d:chronicles_line_numbers +-d:chronicles_runtime_filtering:on