mirror of https://github.com/waku-org/nwaku.git
Waku v2 Node Discovery via DNS: Integration (#690)
This commit is contained in:
parent
1ebcb266cb
commit
36b9176569
|
@ -130,3 +130,8 @@
|
||||||
url = https://github.com/status-im/nim-zlib.git
|
url = https://github.com/status-im/nim-zlib.git
|
||||||
ignore = untracked
|
ignore = untracked
|
||||||
branch = master
|
branch = master
|
||||||
|
[submodule "vendor/nim-dnsdisc"]
|
||||||
|
path = vendor/nim-dnsdisc
|
||||||
|
url = https://github.com/status-im/nim-dnsdisc.git
|
||||||
|
ignore = untracked
|
||||||
|
branch = main
|
||||||
|
|
|
@ -6,6 +6,8 @@ This release contains the following:
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Start of Waku node discovery via DNS following [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- GossipSub [prune backoff period](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#prune-backoff-and-peer-exchange) is now the recommended 1 minute
|
- GossipSub [prune backoff period](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#prune-backoff-and-peer-exchange) is now the recommended 1 minute
|
||||||
|
|
|
@ -16,7 +16,8 @@ import
|
||||||
./v2/test_peer_storage,
|
./v2/test_peer_storage,
|
||||||
./v2/test_waku_keepalive,
|
./v2/test_waku_keepalive,
|
||||||
./v2/test_migration_utils,
|
./v2/test_migration_utils,
|
||||||
./v2/test_namespacing_utils
|
./v2/test_namespacing_utils,
|
||||||
|
./v2/test_waku_dnsdisc
|
||||||
|
|
||||||
when defined(rln):
|
when defined(rln):
|
||||||
import ./v2/test_waku_rln_relay
|
import ./v2/test_waku_rln_relay
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
{.used.}
|
||||||
|
|
||||||
|
import
|
||||||
|
std/[sequtils, tables],
|
||||||
|
chronicles,
|
||||||
|
chronos,
|
||||||
|
testutils/unittests,
|
||||||
|
stew/shims/net,
|
||||||
|
stew/[base32, results],
|
||||||
|
libp2p/crypto/crypto,
|
||||||
|
eth/keys,
|
||||||
|
discovery/dnsdisc/builder,
|
||||||
|
../../waku/v2/node/dnsdisc/waku_dnsdisc,
|
||||||
|
../../waku/v2/node/wakunode2,
|
||||||
|
../test_helpers
|
||||||
|
|
||||||
|
procSuite "Waku DNS Discovery":
|
||||||
|
asyncTest "Waku DNS Discovery end-to-end":
|
||||||
|
## Tests integrated DNS discovery, from building
|
||||||
|
## the tree to connecting to discovered nodes
|
||||||
|
|
||||||
|
# Create nodes and ENR. These will be added to the discoverable list
|
||||||
|
let
|
||||||
|
bindIp = ValidIpAddress.init("0.0.0.0")
|
||||||
|
nodeKey1 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node1 = WakuNode.new(nodeKey1, bindIp, Port(60000))
|
||||||
|
enr1 = node1.enr
|
||||||
|
nodeKey2 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node2 = WakuNode.new(nodeKey2, bindIp, Port(60002))
|
||||||
|
enr2 = node2.enr
|
||||||
|
nodeKey3 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node3 = WakuNode.new(nodeKey3, bindIp, Port(60003))
|
||||||
|
enr3 = node3.enr
|
||||||
|
|
||||||
|
node1.mountRelay()
|
||||||
|
node2.mountRelay()
|
||||||
|
node3.mountRelay()
|
||||||
|
await allFutures([node1.start(), node2.start(), node3.start()])
|
||||||
|
|
||||||
|
# Build and sign tree
|
||||||
|
var tree = buildTree(1, # Seq no
|
||||||
|
@[enr1, enr2, enr3], # ENR entries
|
||||||
|
@[]).get() # No link entries
|
||||||
|
|
||||||
|
let treeKeys = keys.KeyPair.random(rng[])
|
||||||
|
|
||||||
|
# Sign tree
|
||||||
|
check:
|
||||||
|
tree.signTree(treeKeys.seckey()).isOk()
|
||||||
|
|
||||||
|
# Create TXT records at domain
|
||||||
|
let
|
||||||
|
domain = "testnodes.aq"
|
||||||
|
zoneTxts = tree.buildTXT(domain).get()
|
||||||
|
username = Base32.encode(treeKeys.pubkey().toRawCompressed())
|
||||||
|
location = LinkPrefix & username & "@" & domain # See EIP-1459: https://eips.ethereum.org/EIPS/eip-1459
|
||||||
|
|
||||||
|
# Create a resolver for the domain
|
||||||
|
|
||||||
|
proc resolver(domain: string): Future[string] {.async, gcsafe.} =
|
||||||
|
return zoneTxts[domain]
|
||||||
|
|
||||||
|
# Create Waku DNS discovery client on a new Waku v2 node using the resolver
|
||||||
|
|
||||||
|
let
|
||||||
|
nodeKey4 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node4 = WakuNode.new(nodeKey4, bindIp, Port(60004))
|
||||||
|
enr4 = node4.enr
|
||||||
|
|
||||||
|
node4.mountRelay()
|
||||||
|
await node4.start()
|
||||||
|
|
||||||
|
var wakuDnsDisc = WakuDnsDiscovery.init(enr4, location, resolver).get()
|
||||||
|
|
||||||
|
let res = wakuDnsDisc.findPeers()
|
||||||
|
|
||||||
|
check:
|
||||||
|
# We have discovered all three nodes
|
||||||
|
res.isOk()
|
||||||
|
res[].len == 3
|
||||||
|
res[].mapIt(it.peerId).contains(node1.peerInfo.peerId)
|
||||||
|
res[].mapIt(it.peerId).contains(node2.peerInfo.peerId)
|
||||||
|
res[].mapIt(it.peerId).contains(node3.peerInfo.peerId)
|
||||||
|
|
||||||
|
# Connect to discovered nodes
|
||||||
|
await node4.connectToNodes(res[])
|
||||||
|
|
||||||
|
check:
|
||||||
|
# We have successfully connected to all discovered nodes
|
||||||
|
node4.peerManager.peers().anyIt(it.peerId == node1.peerInfo.peerId)
|
||||||
|
node4.peerManager.connectedness(node1.peerInfo.peerId) == Connected
|
||||||
|
node4.peerManager.peers().anyIt(it.peerId == node2.peerInfo.peerId)
|
||||||
|
node4.peerManager.connectedness(node2.peerInfo.peerId) == Connected
|
||||||
|
node4.peerManager.peers().anyIt(it.peerId == node3.peerInfo.peerId)
|
||||||
|
node4.peerManager.connectedness(node3.peerInfo.peerId) == Connected
|
||||||
|
|
||||||
|
await allFutures([node1.stop(), node2.stop(), node3.stop(), node4.stop()])
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit dcb9290d004476fb0a5389baa88121b072abf135
|
|
@ -180,6 +180,18 @@ type
|
||||||
desc: "Enable metrics logging: true|false"
|
desc: "Enable metrics logging: true|false"
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
name: "metrics-logging" }: bool
|
name: "metrics-logging" }: bool
|
||||||
|
|
||||||
|
## DNS discovery config
|
||||||
|
|
||||||
|
dnsDiscovery* {.
|
||||||
|
desc: "Enable discovering nodes via DNS"
|
||||||
|
defaultValue: false
|
||||||
|
name: "dns-discovery" }: bool
|
||||||
|
|
||||||
|
dnsDiscoveryUrl* {.
|
||||||
|
desc: "URL for DNS node list in format 'enrtree://<key>@<fqdn>'",
|
||||||
|
defaultValue: ""
|
||||||
|
name: "dns-discovery-url" }: string
|
||||||
|
|
||||||
# NOTE: Keys are different in nim-libp2p
|
# NOTE: Keys are different in nim-libp2p
|
||||||
proc parseCmdArg*(T: type crypto.PrivateKey, p: TaintedString): T =
|
proc parseCmdArg*(T: type crypto.PrivateKey, p: TaintedString): T =
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
|
## A set of utilities to integrate EIP-1459 DNS-based discovery
|
||||||
|
## for Waku v2 nodes.
|
||||||
|
##
|
||||||
|
## EIP-1459 is defined in https://eips.ethereum.org/EIPS/eip-1459
|
||||||
|
|
||||||
|
import
|
||||||
|
std/options,
|
||||||
|
stew/shims/net,
|
||||||
|
chronicles,
|
||||||
|
chronos,
|
||||||
|
metrics,
|
||||||
|
eth/keys,
|
||||||
|
eth/p2p/discoveryv5/enr,
|
||||||
|
libp2p/crypto/crypto,
|
||||||
|
libp2p/crypto/secp,
|
||||||
|
libp2p/peerinfo,
|
||||||
|
libp2p/multiaddress,
|
||||||
|
discovery/dnsdisc/client
|
||||||
|
|
||||||
|
export client
|
||||||
|
|
||||||
|
declarePublicGauge waku_dnsdisc_discovered, "number of nodes discovered"
|
||||||
|
declarePublicGauge waku_dnsdisc_errors, "number of waku dnsdisc errors", ["type"]
|
||||||
|
|
||||||
|
logScope:
|
||||||
|
topics = "wakudnsdisc"
|
||||||
|
|
||||||
|
type
|
||||||
|
WakuDnsDiscovery* = object
|
||||||
|
enr*: enr.Record
|
||||||
|
client*: Client
|
||||||
|
resolver*: Resolver
|
||||||
|
|
||||||
|
##################
|
||||||
|
# Util functions #
|
||||||
|
##################
|
||||||
|
|
||||||
|
func getTransportProtocol(typedR: TypedRecord): Option[IpTransportProtocol] =
|
||||||
|
if typedR.tcp6.isSome or typedR.tcp.isSome:
|
||||||
|
return some(IpTransportProtocol.tcpProtocol)
|
||||||
|
|
||||||
|
if typedR.udp6.isSome or typedR.udp.isSome:
|
||||||
|
return some(IpTransportProtocol.udpProtocol)
|
||||||
|
|
||||||
|
return none(IpTransportProtocol)
|
||||||
|
|
||||||
|
func toPeerInfo*(enr: enr.Record): Result[PeerInfo, cstring] =
|
||||||
|
let typedR = ? enr.toTypedRecord
|
||||||
|
|
||||||
|
if not typedR.secp256k1.isSome:
|
||||||
|
return err("enr: no secp256k1 key in record")
|
||||||
|
|
||||||
|
let
|
||||||
|
pubKey = ? keys.PublicKey.fromRaw(typedR.secp256k1.get)
|
||||||
|
peerId = ? PeerID.init(crypto.PublicKey(scheme: Secp256k1,
|
||||||
|
skkey: secp.SkPublicKey(pubKey)))
|
||||||
|
|
||||||
|
var addrs = newSeq[MultiAddress]()
|
||||||
|
|
||||||
|
let transportProto = getTransportProtocol(typedR)
|
||||||
|
if transportProto.isNone:
|
||||||
|
return err("enr: could not determine transport protocol")
|
||||||
|
|
||||||
|
case transportProto.get()
|
||||||
|
of tcpProtocol:
|
||||||
|
if typedR.ip.isSome and typedR.tcp.isSome:
|
||||||
|
let ip = ipv4(typedR.ip.get)
|
||||||
|
addrs.add MultiAddress.init(ip, tcpProtocol, Port typedR.tcp.get)
|
||||||
|
|
||||||
|
if typedR.ip6.isSome:
|
||||||
|
let ip = ipv6(typedR.ip6.get)
|
||||||
|
if typedR.tcp6.isSome:
|
||||||
|
addrs.add MultiAddress.init(ip, tcpProtocol, Port typedR.tcp6.get)
|
||||||
|
elif typedR.tcp.isSome:
|
||||||
|
addrs.add MultiAddress.init(ip, tcpProtocol, Port typedR.tcp.get)
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
of udpProtocol:
|
||||||
|
if typedR.ip.isSome and typedR.udp.isSome:
|
||||||
|
let ip = ipv4(typedR.ip.get)
|
||||||
|
addrs.add MultiAddress.init(ip, udpProtocol, Port typedR.udp.get)
|
||||||
|
|
||||||
|
if typedR.ip6.isSome:
|
||||||
|
let ip = ipv6(typedR.ip6.get)
|
||||||
|
if typedR.udp6.isSome:
|
||||||
|
addrs.add MultiAddress.init(ip, udpProtocol, Port typedR.udp6.get)
|
||||||
|
elif typedR.udp.isSome:
|
||||||
|
addrs.add MultiAddress.init(ip, udpProtocol, Port typedR.udp.get)
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
if addrs.len == 0:
|
||||||
|
return err("enr: no addresses in record")
|
||||||
|
|
||||||
|
return ok(PeerInfo.init(peerId, addrs))
|
||||||
|
|
||||||
|
func createEnr*(privateKey: crypto.PrivateKey,
|
||||||
|
enrIp: Option[ValidIpAddress],
|
||||||
|
enrTcpPort, enrUdpPort: Option[Port]): enr.Record =
|
||||||
|
|
||||||
|
assert privateKey.scheme == PKScheme.Secp256k1
|
||||||
|
|
||||||
|
let
|
||||||
|
rawPk = privateKey.getRawBytes().expect("Private key is valid")
|
||||||
|
pk = keys.PrivateKey.fromRaw(rawPk).expect("Raw private key is of valid length")
|
||||||
|
enr = enr.Record.init(1, pk, enrIp, enrTcpPort, enrUdpPort).expect("Record within size limits")
|
||||||
|
|
||||||
|
return enr
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# DNS Discovery API #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
proc emptyResolver*(domain: string): Future[string] {.async, gcsafe.} =
|
||||||
|
debug "Empty resolver called", domain=domain
|
||||||
|
return ""
|
||||||
|
|
||||||
|
proc findPeers*(wdd: var WakuDnsDiscovery): Result[seq[PeerInfo], cstring] =
|
||||||
|
## Find peers to connect to using DNS based discovery
|
||||||
|
|
||||||
|
info "Finding peers using Waku DNS discovery"
|
||||||
|
|
||||||
|
# Synchronise client tree using configured resolver
|
||||||
|
var tree: Tree
|
||||||
|
try:
|
||||||
|
tree = wdd.client.getTree(wdd.resolver) # @TODO: this is currently a blocking operation to not violate memory safety
|
||||||
|
except Exception:
|
||||||
|
error "Failed to synchronise client tree"
|
||||||
|
waku_dnsdisc_errors.inc(labelValues = ["tree_sync_failure"])
|
||||||
|
return err("Node discovery failed")
|
||||||
|
|
||||||
|
let discoveredEnr = wdd.client.getNodeRecords()
|
||||||
|
|
||||||
|
if discoveredEnr.len > 0:
|
||||||
|
info "Successfully discovered ENR", count=discoveredEnr.len
|
||||||
|
else:
|
||||||
|
trace "No ENR retrieved from client tree"
|
||||||
|
|
||||||
|
var discoveredNodes: seq[PeerInfo]
|
||||||
|
|
||||||
|
for enr in discoveredEnr:
|
||||||
|
# Convert discovered ENR to PeerInfo and add to discovered nodes
|
||||||
|
let res = enr.toPeerInfo()
|
||||||
|
|
||||||
|
if res.isOk():
|
||||||
|
discoveredNodes.add(res.get())
|
||||||
|
else:
|
||||||
|
error "Failed to convert ENR to peer info", enr=enr, err=res.error()
|
||||||
|
waku_dnsdisc_errors.inc(labelValues = ["peer_info_failure"])
|
||||||
|
|
||||||
|
if discoveredNodes.len > 0:
|
||||||
|
info "Successfully discovered nodes", count=discoveredNodes.len
|
||||||
|
waku_dnsdisc_discovered.inc(discoveredNodes.len.int64)
|
||||||
|
|
||||||
|
return ok(discoveredNodes)
|
||||||
|
|
||||||
|
proc init*(T: type WakuDnsDiscovery,
|
||||||
|
enr: enr.Record,
|
||||||
|
locationUrl: string,
|
||||||
|
resolver: Resolver): Result[T, cstring] =
|
||||||
|
## Initialise Waku peer discovery via DNS
|
||||||
|
|
||||||
|
debug "init WakuDnsDiscovery", enr=enr, locationUrl=locationUrl
|
||||||
|
|
||||||
|
let
|
||||||
|
client = ? Client.init(locationUrl)
|
||||||
|
wakuDnsDisc = WakuDnsDiscovery(enr: enr, client: client, resolver: resolver)
|
||||||
|
|
||||||
|
debug "init success"
|
||||||
|
|
||||||
|
return ok(wakuDnsDisc)
|
|
@ -5,6 +5,7 @@ import
|
||||||
chronos, chronicles, metrics,
|
chronos, chronicles, metrics,
|
||||||
stew/shims/net as stewNet,
|
stew/shims/net as stewNet,
|
||||||
eth/keys,
|
eth/keys,
|
||||||
|
eth/p2p/discoveryv5/enr,
|
||||||
libp2p/crypto/crypto,
|
libp2p/crypto/crypto,
|
||||||
libp2p/protocols/ping,
|
libp2p/protocols/ping,
|
||||||
libp2p/protocols/pubsub/gossipsub,
|
libp2p/protocols/pubsub/gossipsub,
|
||||||
|
@ -18,7 +19,8 @@ import
|
||||||
../utils/peers,
|
../utils/peers,
|
||||||
../utils/requests,
|
../utils/requests,
|
||||||
./storage/migration/migration_types,
|
./storage/migration/migration_types,
|
||||||
./peer_manager/peer_manager
|
./peer_manager/peer_manager,
|
||||||
|
./dnsdisc/waku_dnsdisc
|
||||||
|
|
||||||
export
|
export
|
||||||
builders,
|
builders,
|
||||||
|
@ -74,6 +76,7 @@ type
|
||||||
wakuRlnRelay*: WakuRLNRelay
|
wakuRlnRelay*: WakuRLNRelay
|
||||||
wakuLightPush*: WakuLightPush
|
wakuLightPush*: WakuLightPush
|
||||||
peerInfo*: PeerInfo
|
peerInfo*: PeerInfo
|
||||||
|
enr*: enr.Record
|
||||||
libp2pPing*: Ping
|
libp2pPing*: Ping
|
||||||
libp2pTransportLoops*: seq[Future[void]]
|
libp2pTransportLoops*: seq[Future[void]]
|
||||||
filters*: Filters
|
filters*: Filters
|
||||||
|
@ -135,6 +138,12 @@ proc new*(T: type WakuNode, nodeKey: crypto.PrivateKey,
|
||||||
announcedAddresses = if extIp.isNone() or extPort.isNone(): @[]
|
announcedAddresses = if extIp.isNone() or extPort.isNone(): @[]
|
||||||
else: @[tcpEndPoint(extIp.get(), extPort.get())]
|
else: @[tcpEndPoint(extIp.get(), extPort.get())]
|
||||||
peerInfo = PeerInfo.init(nodekey)
|
peerInfo = PeerInfo.init(nodekey)
|
||||||
|
enrIp = if extIp.isSome(): extIp
|
||||||
|
else: some(bindIp)
|
||||||
|
enrTcpPort = if extPort.isSome(): extPort
|
||||||
|
else: some(bindPort)
|
||||||
|
enr = createEnr(nodeKey, enrIp, enrTcpPort, none(Port))
|
||||||
|
|
||||||
info "Initializing networking", hostAddress,
|
info "Initializing networking", hostAddress,
|
||||||
announcedAddresses
|
announcedAddresses
|
||||||
# XXX: Add this when we create node or start it?
|
# XXX: Add this when we create node or start it?
|
||||||
|
@ -156,6 +165,7 @@ proc new*(T: type WakuNode, nodeKey: crypto.PrivateKey,
|
||||||
switch: switch,
|
switch: switch,
|
||||||
rng: rng,
|
rng: rng,
|
||||||
peerInfo: peerInfo,
|
peerInfo: peerInfo,
|
||||||
|
enr: enr,
|
||||||
filters: initTable[string, Filter]()
|
filters: initTable[string, Filter]()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -659,6 +669,7 @@ proc start*(node: WakuNode) {.async.} =
|
||||||
let listenStr = $peerInfo.addrs[^1] & "/p2p/" & $peerInfo.peerId
|
let listenStr = $peerInfo.addrs[^1] & "/p2p/" & $peerInfo.peerId
|
||||||
## XXX: this should be /ip4..., / stripped?
|
## XXX: this should be /ip4..., / stripped?
|
||||||
info "Listening on", full = listenStr
|
info "Listening on", full = listenStr
|
||||||
|
info "Discoverable ENR ", enr = node.enr.toURI()
|
||||||
|
|
||||||
if not node.wakuRelay.isNil:
|
if not node.wakuRelay.isNil:
|
||||||
await node.startRelay()
|
await node.startRelay()
|
||||||
|
@ -838,6 +849,22 @@ when isMainModule:
|
||||||
# Connect to configured static nodes
|
# Connect to configured static nodes
|
||||||
if conf.staticnodes.len > 0:
|
if conf.staticnodes.len > 0:
|
||||||
waitFor connectToNodes(node, conf.staticnodes)
|
waitFor connectToNodes(node, conf.staticnodes)
|
||||||
|
|
||||||
|
# Connect to discovered nodes
|
||||||
|
if conf.dnsDiscovery and conf.dnsDiscoveryUrl != "":
|
||||||
|
# @ TODO: this is merely POC integration with an empty resolver
|
||||||
|
debug "Waku DNS Discovery enabled. Using empty resolver."
|
||||||
|
|
||||||
|
var wakuDnsDiscovery = WakuDnsDiscovery.init(node.enr,
|
||||||
|
conf.dnsDiscoveryUrl,
|
||||||
|
emptyResolver) # TODO: Add DNS resolver
|
||||||
|
if wakuDnsDiscovery.isOk:
|
||||||
|
let discoveredPeers = wakuDnsDiscovery.get().findPeers()
|
||||||
|
if discoveredPeers.isOk:
|
||||||
|
info "Connecting to discovered peers"
|
||||||
|
waitFor connectToNodes(node, discoveredPeers.get())
|
||||||
|
else:
|
||||||
|
warn "Failed to init Waku DNS discovery"
|
||||||
|
|
||||||
# Start keepalive, if enabled
|
# Start keepalive, if enabled
|
||||||
if conf.keepAlive:
|
if conf.keepAlive:
|
||||||
|
|
Loading…
Reference in New Issue