Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cbindings): first commit - waku relay (#1632) #1714

Merged
merged 11 commits into from
May 12, 2023

Conversation

Ivansete-status
Copy link
Collaborator

@Ivansete-status Ivansete-status commented May 4, 2023

Description

This allows the creation of a libcwakuv2.a library which exposes the next Waku functions:

waku_version()
waku_set_event_callback(..)
waku_content_topic(..)
waku_pubsub_topic(..)
waku_default_pubsub_topic(..)
waku_relay_publish(..)
waku_start()
waku_stop()
waku_relay_subscribe(..)
waku_relay_unsubscribe(..)
waku_connect(..)
waku_poll()

Changes

  • Have a static library (libwaku.a) for unix systems.
  • Have a C program that links the Nwaku library. That program should mount the Relay protocol, allowing:
    - subscribe/unsubscribe from a PubSub topic.
    - send a message to a PubSub topic.

How to test

  1. Build the cwaku_example with:
    make cwaku_example -j$(cat /proc/cpuinfo | grep processor | wc -l)
    note: on completion, the cwaku_example binary and the libwaku.a library will be in the build folder.

  2. Open a terminal and run:
    ./build/cwaku_example --peers=/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN --port=60000 --key=364d111d729a6eb6d2e6113e163f017b5ef03a6f94c9b5b7bb1bb36fa5cb07a9

  3. Open a terminal and run:
    ./build/cwaku_example --peers=/ip4/127.0.0.1/tcp/60000/p2p/16Uiu2HAm2eqzqp6xn32fzgGi8K4BuF88W4Xy6yxsmDcW8h1gj6ie --port=60001 --key=0d714a1fada214dead6dc9c7274585eca0ff292451866e7d6d677dc818e8ccd2

  4. Subscribe both nodes to the same pubsubtopic and from one node, publish a message. For that, follow the instructions that are shown in the naïve menu.
    i.e.

Please, select an option:
	1.) Subscribe to topic
	2.) Connect to other node
	3.) Publish a message

Issue

#1632

Pending tasks

  1. Generate dynamic library
  2. Pending to know how to retrieve the message_ID when publishing a new message
  3. Pending to implement the publish_enc_symmetric and publish_enc_asymmetric, which is already done in go-waku.
  4. Pending to implement waku_relay_enough_peers(char* pubsubTopic). This feature is not currently implemented in
    libp2p-1.0.0/libp2p/protocols/pubsub/gossipsub.nim.
  5. @richard-ramos - It might be interesting to offer a show_version function in the rfc. That would be helpful for users to know the library's version. This function is given in this PR.

Copy link
Member

@richard-ramos richard-ramos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool, i like this PR.
I just left some comments with minor observations and items for discussion

app.node.wakuRelay.unsubscribeAll(PubsubTopic($pubSubTopic))

proc publishMessage*(app: App, pubSubTopic: cstring, message: WakuMessage): Future[int] {.gcsafe, async.} =
# Returns the number of peers connected to the given pubSubTopic.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Returns the number of peers connected to the given pubSubTopic.

else:
jsonResp = errResp("Timeout expired")

proc init(config_file: cstring) {.dynlib, exportc.} =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be renamed to waku_new and waku_start? that way we follow the waku_* convention. The same goes for show_version


const versionString = "version / git commit hash: " & app.git_version
{.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError
proc init*(configFilePath = "") =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see init here uses a path vs the rfc which suggests a json string. We should decide which approach to use to unify behaviors between implementations. cc: @danielSanchezQ for his thoughts as he has worked with the rust+go bindings before

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should init an app here, but operate on the WakuNode object as defined in waku_node.nim (see comments elsewhere). wakunode2 is simply an application written around the Nim API. As stated elsewhere, we want to provide C bindings wrapping the Nim API, not the wakunode2 application

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see init here uses a path vs the rfc which suggests a json string. We should decide which approach to use to unify behaviors between implementations. cc: @danielSanchezQ for his thoughts as he has worked with the rust+go bindings before

IMO using a configuration is better. A config file should be used for a binary, a configuration object for a library.

@jm-clius Where can I find the nim api documentation? Don't know if Im understanding correctly, but, does it mean that the wakunode2 is an external application running and the bindings just communicate with it. Or do this works in a similar way to go-waku in where we do embedded the node within the application?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The application simply provides a way to configure and instantiate the node, so I guess more similar to "embedded within the application". The Nim API documentation is sparse and outdated (since it's only used internally), but in theory lives here: https://github.com/waku-org/nwaku/blob/master/docs/api/v2/node.md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's very outdated, in fact. 😟 @Ivansete-status perhaps a good idea that we clean the Nim API doc up at some point during the development of the C bindings, since we're likely to expand/change it in any case.

exit(-1);
}

NimMain(); // initialize the Nim runtime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably specify in the RFC or in the .h docs that NimMain() must be executed when using the nim c-bindings

Comment on lines 58 to 59
proc show_version(): cstring {.dynlib, exportc.} =
return wakuNode2VersionString
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be waku_version(). waku_node already provides git_version, so I don't think this needs to be exported elsewhere. If we need to construct something specific, that should be done on the Nim API in waku_node.nim.

Comment on lines 121 to 124
for i in 0 .. timeoutMs:
if pubMsgFut.finished():
break
sleep(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chronos has a withTimeout function that i think could be used here instead of sleep:
https://github.com/status-im/nim-chronos/blob/master/chronos/asyncloop.nim#L1116

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super interesting @richard-ramos. Thanks for that!
However, we cannot invoke await, and therefore, return a Future[..] in this proc because it is exported to C and this call needs to be synchronous.

Comment on lines 101 to 108
wakuMessage = WakuMessage(
# Visit https://rfc.vac.dev/spec/14/ for further details
payload: jsonContent["payload"].getStr().toSeq().mapIt(byte (it)),
contentTopic: $jsonContent["content_topic"].getStr(),
version: version,
timestamp: getTime().toUnix(),
ephemeral: false
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to specify these values instead of having them hardcoded. The meta field is also missing https://github.com/waku-org/nwaku/blob/master/waku/v2/waku_core/message/message.nim#L28 (this field should probably be added to the bindings rfc)

payload: payload,
contentTopic: msg.contentTopic,
version: msg.version,
timestamp: int64(msg.timestamp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some fields from WakuMessage are missing. Maybe it also makes sense to include the proof if RLN is enabled?

What do you think if instead of creating a JsonMessage with the WakuMessage content, to instead serialize it with https://github.com/status-im/nim-json-serialization? I imagine it should be possible to deserialize it too in waku_relay_publish. The advantage of doing that is that if in the future new fields are added to WakuMessage protobuffer, they'll be serialized/unserialized automatically in the c-bindings API functions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I absolutely agree. Nevertheless, I added the fields indicated in the next:
https://rfc.vac.dev/spec/36/#jsonmessage-type

# https://rfc.vac.dev/spec/36/#jsonmessageevent-type

var payload = newString(len(msg.payload))
copyMem(addr payload[0], unsafeAddr msg.payload[0], len(msg.payload))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this line. Why is it necessary to copy the payload like this instead of doing it directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I will try to directly use seq[byte] instead

Copy link
Collaborator Author

@Ivansete-status Ivansete-status May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will leave it as is because if I change it to use seq[byte] we don't get a readable base64 string. In this case, it appears sth like "payload":[97,71,57,115,89,81,61,61]

@Ivansete-status Ivansete-status self-assigned this May 5, 2023
Copy link
Contributor

@jm-clius jm-clius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see progress here and thanks for including an example! Most important directional change I suggest is to keep the interface between the WakuNode and the wakunode2 application very clean. Our library is the WakuNode (defined in waku_node.nim) and it already exposes a Nim API (as well as JSON, REST, etc.). We want to provide C bindings for this library (more or less just wrapping the Nim API) not the wakunode2 application.

Makefile Outdated
################
.PHONY: cbindings cwakuv2example

libcwakuv2.a: | build deps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move away from always having to specify v2 for Waku. Waku v1 is deprecated, in a sense, and not used anywhere. We even want to remove Waku v1 code from this repo, so it becomes simpler if we start referring to Waku v2 simply as the Waku. In other words, I think this can be libwaku (just replacing the name occupied by previous unused target).

@@ -92,20 +94,19 @@ when isMainModule:
error "4/7 Mounting protocols failed", error=res5.error
quit(QuitFailure)

proc startNode*() =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite confused about this. Does this split the application's setup into an init and start phase?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exaclty, that is a simple split. However that might not be needed, and I will likely revert it back, when I wrap the waku_node.nim library instead of the wakunode2 app.

Comment on lines 154 to 166
proc subscribeCallbackToTopic*(pubSubTopic: cstring,
callback: proc(pubsubTopic: string, data: seq[byte]): Future[void] {.gcsafe, raises: [Defect].}) =
wakunode2.subscribeCallbackToTopic(pubSubTopic, callback)

proc unsubscribeCallbackFromTopic*(pubSubTopic: cstring,
callback: proc(pubsubTopic: string, data: seq[byte]): Future[void] {.gcsafe, raises: [Defect].}) =
wakunode2.unsubscribeCallbackFromTopic(pubSubTopic, callback)

proc unsubscribeAllCallbacksFromTopic*(pubSubTopic: cstring) =
wakunode2.unsubscribeAllCallbackFromTopic(pubSubTopic)

proc publishMessage*(pubSubTopic: cstring, message: WakuMessage): Future[int] {.gcsafe, async.} =
return await wakunode2.publishMessage(pubSubTopic, message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I see it, these do not belong here. The C bindings should be a wrapper around the Nim API defined in waku_node.nim. As such the app modules should remain unchanged and preferably unimported elsewhere. Any additional API functionality should go to waku_node.nim. Of course, since we need a configured WakuNode object for those bindings, we may have to provide App-like functionality when initiating the node. For now, though I think the configuration will be much, much simpler than is needed for a wakunode2 application and providing the basic configuration in a JsonConfig via waku_new() will be enough.

Comment on lines 58 to 59
proc show_version(): cstring {.dynlib, exportc.} =
return wakuNode2VersionString
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be waku_version(). waku_node already provides git_version, so I don't think this needs to be exported elsewhere. If we need to construct something specific, that should be done on the Nim API in waku_node.nim.


const versionString = "version / git commit hash: " & app.git_version
{.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError
proc init*(configFilePath = "") =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should init an app here, but operate on the WakuNode object as defined in waku_node.nim (see comments elsewhere). wakunode2 is simply an application written around the Nim API. As stated elsewhere, we want to provide C bindings wrapping the Nim API, not the wakunode2 application

@Ivansete-status
Copy link
Collaborator Author

Ivansete-status commented May 11, 2023

Thanks for the comments ! It is now ready to be reviewed again :)

Btw, very happy for the hint that you shared with me @LNSD. Thanks for that! The approach that is applied in this PR is inspired by https://github.com/KomodoPlatform/nimbus/blob/master/wrappers/wrapper_example.c

Copy link
Contributor

@jm-clius jm-clius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great work with this. Well done! I've added a couple of non-blocking comments. Feel free to merge once those are addressed.

I would suggest thinking of ways in which we can split the logic into separate modules in future (the main library module is going to grow very large!) I'd suggest using the REST API as inspiration and separate relay, store and other protocol bindings as well as types, serdes, etc. For now this is OK as a starting point.

library/waku.nim Outdated
chronos,
libp2p/crypto/secp,
stew/shims/net,
eth/net/nat as todo_delete_this_module
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will clean the setupNat, once this is merged, in a separate PR :)

library/waku.nim Outdated
../../waku/v2/waku_core/message/message,
../../waku/v2/waku_core/topics/pubsub_topic,
../../waku/v2/node/peer_manager/peer_manager,
../../waku/v2/node/waku_node as waku_node_module,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: is this alias necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, is not needed. Thanks!

library/waku.nim Outdated
@@ -0,0 +1,399 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider calling this module something like libwaku.

library/waku.nim Outdated
Comment on lines 261 to 263
proc waku_pubsub_topic(topicName: cstring, encoding: cstring, outPubsubTopic: var string) {.dynlib, exportc.} =
# https://rfc.vac.dev/spec/36/#extern-char-waku_pubsub_topicchar-name-char-encoding
outPubsubTopic = fmt"/waku/2/{topicName}/{encoding}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that for pubsub topics we've deprecated the {encoding} suffix (https://rfc.vac.dev/spec/23/#pubsub-topic-format).

library/waku.nim Outdated
Comment on lines 385 to 387
let connectFut = node.connectToNodes(peers, source="static")
while not connectFut.finished():
poll()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, where does timeoutMs come in here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this is something we need to fix from lower levels. I'll add a TODO statement here.

library/waku.nim Outdated
# }
return $(%* { "error": message })

proc setupNat(natConf, clientId: string, tcpPort, udpPort: Port):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must be missing some details here, but why wouldn't setupNat in the common utility work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setupNat in the common is deprecated with the next comment: deprecated: "Unsafe: this proc quits the app if something is not ok".

Once this PR is fixed I want to raise a separate PR to fix the setupNat proc from common and only use it. Also, clean the eth/net/nat as todo_delete_this_module :)

@@ -71,6 +70,7 @@ const clientId* = "Nimbus Waku v2 node"
# Default Waku Filter Timeout
const WakuFilterTimeout: Duration = 1.days

const wakuNode2VersionString* = "version / git commit hash: " & git_version
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not followed everywhere, but we want constants to be in PascalCase (the exception are for those overridden by compiler definitions, such as git_version above).
This would just be WakuNodeVersionString (not WakuNode2)

Copy link
Contributor

@jm-clius jm-clius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great work with this. Well done! I've added a couple of non-blocking comments. Feel free to merge once those are addressed.

I would suggest thinking of ways in which we can split the logic into separate modules in future (the main library module is going to grow very large!) I'd suggest using the REST API as inspiration and separate relay, store and other protocol bindings as well as types, serdes, etc. For now this is OK as a starting point.

Copy link
Member

@richard-ramos richard-ramos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Comment on lines 396 to 397
proc waku_poll() {.dynlib, exportc.} =
poll()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this function used for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The poll() function is part of the asynchronous chronos library. It is needed to make the Nim runtime to take steps further.
For each call to poll() checks whether there are OS signals, or network events. This is therefore needed to make the Nwaku active so that it can attend and reply requests.

https://github.com/status-im/nim-chronos/blob/ab5a8c2e0f6941fe3debd61dff0293790079d1b0/chronos/asyncloop.nim#LL822C16-L822C26

For example, in the Nwaku app, there is a runForever() call that is an infinite loop around poll().
https://github.com/waku-org/nwaku/blob/2ec9809cf31b202ee705d337cf3384fe0f7374f4/apps/wakunode2/wakunode2.nim#LL151C4-L151C4

Comment on lines 364 to 369
if node.wakuRelay == nil:
jsonResp = errResp("""Cannot unsubscribe without a callback.
Kindly set it with the 'waku_set_event_callback' function""")
return false

if node.wakuRelay.isNil():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for learning purposes, what's the difference between wakuRelay == nil and wakuRelay.isNil()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point my friend! Actually you discovered a bug hehe.

It seems isNil() is more efficient, so I'd rather use it from now on.

proc isNil*[T](x: ptr T): bool {.noSideEffect, magic: "IsNil".}
proc isNil*(x: pointer): bool {.noSideEffect, magic: "IsNil".}
proc isNil*(x: cstring): bool {.noSideEffect, magic: "IsNil".}
proc isNil*[T: proc](x: T): bool {.noSideEffect, magic: "IsNil".}
Fast check whether x is nil. This is sometimes more efficient than ## == nil.

@Ivansete-status Ivansete-status merged commit 2defbd2 into master May 12, 2023
@Ivansete-status Ivansete-status deleted the feat-1632-cbindings-relay branch May 12, 2023 16:08
@vpavlin vpavlin mentioned this pull request May 16, 2023
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants