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

chore: Discovery in libwaku #2711

Merged
merged 3 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions examples/cbindings/waku_example.c
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ int main(int argc, char** argv) {
show_help_and_exit();
}

char jsonConfig[2048];
snprintf(jsonConfig, 2048, "{ \
char jsonConfig[5000];
snprintf(jsonConfig, 5000, "{ \
\"listenAddress\": \"%s\", \
\"tcpPort\": %d, \
\"nodekey\": \"%s\", \
Expand All @@ -277,7 +277,14 @@ int main(int argc, char** argv) {
\"storeMessageDbUrl\": \"%s\", \
\"storeMessageRetentionPolicy\": \"%s\", \
\"storeMaxNumDbConnections\": %d , \
\"logLevel\": \"DEBUG\" \
\"logLevel\": \"DEBUG\", \
\"discv5Discovery\": true, \
\"discv5BootstrapNodes\": \
[\"enr:-QESuEB4Dchgjn7gfAvwB00CxTA-nGiyk-aALI-H4dYSZD3rUk7bZHmP8d2U6xDiQ2vZffpo45Jp7zKNdnwDUx6g4o6XAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOvD3S3jUNICsrOILlmhENiWAMmMVlAl6-Q8wRB7hidY4N0Y3CCdl-DdWRwgiMohXdha3UyDw\", \"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\"], \
\"discv5UdpPort\": 9999, \
\"dnsDiscovery\": true, \
\"dnsDiscoveryUrl\": \"enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im\", \
\"dnsDiscoveryNameServers\": [\"8.8.8.8\", \"1.0.0.1\"] \
}", cfgNode.host,
cfgNode.port,
cfgNode.key,
Expand Down Expand Up @@ -313,6 +320,11 @@ int main(int argc, char** argv) {
event_handler,
userData) );

WAKU_CALL( waku_discv5_update_bootnodes(ctx,
"[\"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\",\"enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw\"]",
event_handler,
userData) );

show_main_menu();
while(1) {
handle_user_input();
Expand Down
19 changes: 19 additions & 0 deletions library/libwaku.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ int waku_listen_addresses(void* ctx,
WakuCallBack callback,
void* userData);

// Returns a list of multiaddress given a url to a DNS discoverable ENR tree
// Parameters
// char* entTreeUrl: URL containing a discoverable ENR tree
// char* nameDnsServer: The nameserver to resolve the ENR tree url.
// int timeoutMs: Timeout value in milliseconds to execute the call.
int waku_dns_discovery(void* ctx,
const char* entTreeUrl,
const char* nameDnsServer,
int timeoutMs,
WakuCallBack callback,
void* userData);

// Updates the bootnode list used for discovering new peers via DiscoveryV5
// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]`
int waku_discv5_update_bootnodes(void* ctx,
char* bootnodes,
WakuCallBack callback,
void* userData);

#ifdef __cplusplus
}
#endif
Expand Down
48 changes: 48 additions & 0 deletions library/libwaku.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import
./waku_thread/inter_thread_communication/requests/protocols/relay_request,
./waku_thread/inter_thread_communication/requests/protocols/store_request,
./waku_thread/inter_thread_communication/requests/debug_node_request,
./waku_thread/inter_thread_communication/requests/discovery_request,
./waku_thread/inter_thread_communication/waku_thread_request,
./alloc,
./callback
Expand Down Expand Up @@ -397,5 +398,52 @@ proc waku_listen_addresses(
callback(RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_OK

proc waku_dns_discovery(
ctx: ptr Context,
entTreeUrl: cstring,
nameDnsServer: cstring,
timeoutMs: cint,
callback: WakuCallBack,
userData: pointer,
): cint {.dynlib, exportc.} =
ctx[].userData = userData

let bootstrapPeers = waku_thread.sendRequestToWakuThread(
ctx,
RequestType.DISCOVERY,
DiscoveryRequest.createRetrieveBootstrapNodesRequest(
DiscoveryMsgType.GET_BOOTSTRAP_NODES, entTreeUrl, nameDnsServer, timeoutMs
),
).valueOr:
let msg = $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_ERR

let msg = $bootstrapPeers
callback(RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_OK

proc waku_discv5_update_bootnodes(
ctx: ptr Context, bootnodes: cstring, callback: WakuCallBack, userData: pointer
): cint {.dynlib, exportc.} =
## Updates the bootnode list used for discovering new peers via DiscoveryV5
## bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]`
ctx[].userData = userData

let resp = waku_thread.sendRequestToWakuThread(
ctx,
RequestType.DISCOVERY,
DiscoveryRequest.createUpdateBootstrapNodesRequest(
DiscoveryMsgType.UPDATE_DISCV5_BOOTSTRAP_NODES, bootnodes
),
).valueOr:
let msg = $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_ERR

let msg = $resp
callback(RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_OK

### End of exported procs
################################################################################
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import std/[json, sequtils]
import chronos, stew/results, libp2p/multiaddress
import
../../../../waku/factory/waku,
../../../../waku/discovery/waku_dnsdisc,
../../../../waku/discovery/waku_discv5,
../../../../waku/waku_core/peers,
../../../alloc

type DiscoveryMsgType* = enum
GET_BOOTSTRAP_NODES
UPDATE_DISCV5_BOOTSTRAP_NODES

type DiscoveryRequest* = object
operation: DiscoveryMsgType

## used in GET_BOOTSTRAP_NODES
enrTreeUrl: cstring
nameDnsServer: cstring
timeoutMs: cint

## used in UPDATE_DISCV5_BOOTSTRAP_NODES
nodes: cstring

proc createShared(
T: type DiscoveryRequest,
op: DiscoveryMsgType,
enrTreeUrl: cstring,
nameDnsServer: cstring,
timeoutMs: cint,
nodes: cstring,
): ptr type T =
var ret = createShared(T)
ret[].operation = op
ret[].enrTreeUrl = enrTreeUrl.alloc()
ret[].nameDnsServer = nameDnsServer.alloc()
ret[].timeoutMs = timeoutMs
ret[].nodes = nodes.alloc()
return ret

proc createRetrieveBootstrapNodesRequest*(
T: type DiscoveryRequest,
op: DiscoveryMsgType,
enrTreeUrl: cstring,
nameDnsServer: cstring,
timeoutMs: cint,
): ptr type T =
return T.createShared(op, enrTreeUrl, nameDnsServer, timeoutMs, "")

proc createUpdateBootstrapNodesRequest*(
T: type DiscoveryRequest, op: DiscoveryMsgType, nodes: cstring
): ptr type T =
return T.createShared(op, "", "", 0, nodes)

proc destroyShared(self: ptr DiscoveryRequest) =
deallocShared(self[].enrTreeUrl)
deallocShared(self[].nameDnsServer)
deallocShared(self)

proc retrieveBootstrapNodes(
enrTreeUrl: string, ipDnsServer: string
): Result[seq[string], string] =
let dnsNameServers = @[parseIpAddress(ipDnsServer)]
let discoveredPeers: seq[RemotePeerInfo] = retrieveDynamicBootstrapNodes(
true, enrTreeUrl, dnsNameServers
).valueOr:
return err("failed discovering peers from DNS: " & $error)

var multiAddresses = newSeq[string]()

for discPeer in discoveredPeers:
for address in discPeer.addrs:
multiAddresses.add($address & "/" & $discPeer)

return ok(multiAddresses)

proc updateDiscv5BootstrapNodes(nodes: string, waku: ptr Waku): Result[void, string] =
waku.wakuDiscv5.updateBootstrapRecords(nodes).isOkOr:
return err("error in updateDiscv5BootstrapNodes: " & $error)
return ok()

proc process*(
self: ptr DiscoveryRequest, waku: ptr Waku
): Future[Result[string, string]] {.async.} =
defer:
destroyShared(self)

case self.operation
of GET_BOOTSTRAP_NODES:
let nodes = retrieveBootstrapNodes($self[].enrTreeUrl, $self[].nameDnsServer).valueOr:
return err($error)

return ok($(%*nodes))
of UPDATE_DISCV5_BOOTSTRAP_NODES:
updateDiscv5BootstrapNodes($self[].nodes, waku).isOkOr:
return err($error)
return ok("discovery request processed correctly")

return err("discovery request not handled")
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,28 @@ proc createWaku(configJson: cstring): Future[Result[Waku, string]] {.async.} =

var errorResp: string

var jsonNode: JsonNode
try:
let jsonNode = parseJson($configJson)
jsonNode = parseJson($configJson)
except Exception:
return err(
"exception in createWaku when calling parseJson: " & getCurrentExceptionMsg() &
" configJson string: " & $configJson
)

for confField, confValue in fieldPairs(conf):
if jsonNode.contains(confField):
# Make sure string doesn't contain the leading or trailing " character
let formattedString = ($jsonNode[confField]).strip(chars = {'\"'})
# Override conf field with the value set in the json-string
for confField, confValue in fieldPairs(conf):
if jsonNode.contains(confField):
# Make sure string doesn't contain the leading or trailing " character
let formattedString = ($jsonNode[confField]).strip(chars = {'\"'})
# Override conf field with the value set in the json-string
try:
confValue = parseCmdArg(typeof(confValue), formattedString)
except Exception:
return err("exception parsing configuration: " & getCurrentExceptionMsg())
except Exception:
return err(
"exception in createWaku when parsing configuration. exc: " &
getCurrentExceptionMsg() & ". string that could not be parsed: " &
formattedString & ". expected type: " & $typeof(confValue)
)

let wakuRes = Waku.init(conf).valueOr:
error "waku initialization failed", error = error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import
./requests/peer_manager_request,
./requests/protocols/relay_request,
./requests/protocols/store_request,
./requests/debug_node_request
./requests/debug_node_request,
./requests/discovery_request

type RequestType* {.pure.} = enum
LIFECYCLE
PEER_MANAGER
RELAY
STORE
DEBUG
DISCOVERY

type InterThreadRequest* = object
reqType: RequestType
Expand Down Expand Up @@ -52,6 +54,8 @@ proc process*(
cast[ptr StoreRequest](request[].reqContent).process(waku)
of DEBUG:
cast[ptr DebugNodeRequest](request[].reqContent).process(waku[])
of DISCOVERY:
cast[ptr DiscoveryRequest](request[].reqContent).process(waku)

return await retFut

Expand Down
27 changes: 26 additions & 1 deletion waku/discovery/waku_discv5.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ else:
{.push raises: [].}

import
std/[sequtils, strutils, options, sets, net],
std/[sequtils, strutils, options, sets, net, json],
stew/results,
chronos,
chronicles,
Expand Down Expand Up @@ -373,3 +373,28 @@ proc setupDiscoveryV5*(
WakuDiscoveryV5.new(
rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue
)

proc updateBootstrapRecords*(
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any value in updating bootstrap nodes on runtime? aren't they only used when connecting to the network?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is there any value in updating bootstrap nodes on runtime? aren't they only used when connecting to the network?

Good point! This feature comes from what we are already offering from go-waku so we offer the same. I guess that was a requirement from any of the current projects that integrates us (cc @richard-ramos .)

Copy link
Member

Choose a reason for hiding this comment

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

To give some context (since this might be implementation-specific in go-waku)

For discv5, we use the go-ethereum implementation (which i extracted into a separate package in https://github.com/waku-org/go-discover). There are two separate instances in which discv5 requires populating the bootnodes in runtime:

1 - When starting the node, sometimes the DNS Discovery URL fails to resolve (i.e. maybe the nameserver is down). Since in Status app we shoudn't stop the login process unless it is an unrecoverable error, we do start discv5, (which will not return any peer), and keep retrying with dns discovery until the address resolves, and in that moment we setup the bootnodes.

  1. One thing we noticed from the go-ethereum implementation is that if you get disconnected for a while, the routing tables from discv5 remove all the nodes including the bootnodes. So, when that happens, we do add the bootnodes back to discv5. Perhaps this is something that does not happen in nwaku's discv5?

Copy link
Contributor

Choose a reason for hiding this comment

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

dns discovery

are there nodes that do not require dns discovery in the bootstrap node list?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dns discovery

are there nodes that do not require dns discovery in the bootstrap node list?

In case you asked w.r.t wakunode2 (nwaku), it is mandatory for the app to retrieve bootstrap nodes from DNS:

nwaku/waku/factory/waku.nim

Lines 140 to 161 in 5ee4cba

let dynamicBootstrapNodesRes = waku_dnsdisc.retrieveDynamicBootstrapNodes(
confCopy.dnsDiscovery, confCopy.dnsDiscoveryUrl, confCopy.dnsDiscoveryNameServers
)
if dynamicBootstrapNodesRes.isErr():
error "Retrieving dynamic bootstrap nodes failed",
error = dynamicBootstrapNodesRes.error
return err(
"Retrieving dynamic bootstrap nodes failed: " & dynamicBootstrapNodesRes.error
)
let nodeRes = setupNode(confCopy, some(rng))
if nodeRes.isErr():
error "Failed setting up node", error = nodeRes.error
return err("Failed setting up node: " & nodeRes.error)
var waku = Waku(
version: git_version,
conf: confCopy,
rng: rng,
key: confCopy.nodekey.get(),
node: nodeRes.get(),
dynamicBootstrapNodes: dynamicBootstrapNodesRes.get(),

Copy link
Contributor

Choose a reason for hiding this comment

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

don't know the details then, but that sounds like an unnecessary single point of failure - normally, one would bootstrap discv5 from multiple sources (dns, hardcoded nodes, nodes from previous startups etc)

Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that it's not mandatory. If we don't have DNS discovery enabled retrieveDynamicBootstrapNodes will return ok with an empty seq and the node will simply set up without dynamic bootstrap nodes.

debug "No method for retrieving dynamic bootstrap nodes specified."
ok(newSeq[RemotePeerInfo]()) # Return an empty seq by default

However, if we enable DNS discovery and it fails retrieving the nodes then the whole node setup fails. Not sure if it should be like that or if it would be better to add a warning and continue the setup using other sources as a backup (as if DNS discovery wasn't configured)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the comment @gabrielmer ! You are absolutely right with that :) With that, the dns source is not mandatory.

On the other hand, we also have another possible source of bootstrap nodes for discv5, which is a config parameter:

discv5BootstrapNodes* {.
desc:
"Text-encoded ENR for bootstrap node. Used when connecting to the network. Argument may be repeated.",
name: "discv5-bootstrap-node"
.}: seq[string]

( cc @arnetheduck )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure if it should be like that or if it would be better to add a warning and continue the setup using other sources as a backup (as if DNS discovery wasn't configured)

@gabrielmer - very good point!

It might sound better to only fail everything if discv5 tries to start without any bootstrap node. Nevertheless, I think is better to leave it as it is now because that can help to rapidly identify DNS issues. If in the future we see that this represent a blocking/repetitive issue then we can reconsider the current approach :)

Cheers

self: var WakuDiscoveryV5, newRecordsString: string
): Result[void, string] =
## newRecordsString - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]`
var newRecords = newSeq[waku_enr.Record]()

var jsonNode: JsonNode
try:
jsonNode = parseJson(newRecordsString)
except Exception:
return err("exception parsing json enr records: " & getCurrentExceptionMsg())

if jsonNode.kind != JArray:
return err("updateBootstrapRecords should receive a json array containing ENRs")

for enr in jsonNode:
let enrWithoutQuotes = ($enr).replace("\"", "")
var bootstrapNodeEnr: waku_enr.Record
if not bootstrapNodeEnr.fromURI(enrWithoutQuotes):
return err("wrong enr given: " & enrWithoutQuotes)

self.protocol.bootstrapRecords = newRecords

return ok()
Loading