mirror of https://github.com/waku-org/nwaku.git
feat(relay): add support for gossipsub peer exchange (#911)
This commit is contained in:
parent
d0cf3ed1f9
commit
52de0e434f
|
@ -21,7 +21,8 @@ import
|
||||||
./v2/test_waku_discv5,
|
./v2/test_waku_discv5,
|
||||||
./v2/test_enr_utils,
|
./v2/test_enr_utils,
|
||||||
./v2/test_waku_store_queue,
|
./v2/test_waku_store_queue,
|
||||||
./v2/test_pagination_utils
|
./v2/test_pagination_utils,
|
||||||
|
./v2/test_peer_exchange
|
||||||
|
|
||||||
when defined(rln):
|
when defined(rln):
|
||||||
import ./v2/test_waku_rln_relay
|
import ./v2/test_waku_rln_relay
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
{.used.}
|
||||||
|
|
||||||
|
import
|
||||||
|
std/[sequtils, options],
|
||||||
|
chronicles,
|
||||||
|
chronos,
|
||||||
|
libp2p/crypto/crypto,
|
||||||
|
libp2p/protocols/pubsub/gossipsub,
|
||||||
|
stew/shims/net,
|
||||||
|
testutils/unittests,
|
||||||
|
../../waku/v2/node/wakunode2,
|
||||||
|
../test_helpers
|
||||||
|
|
||||||
|
procSuite "Peer Exchange":
|
||||||
|
asyncTest "GossipSub (relay) peer exchange":
|
||||||
|
## Tests peer exchange
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
nodeKey2 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node2 = WakuNode.new(nodeKey2, bindIp, Port(60002), sendSignedPeerRecord = true)
|
||||||
|
nodeKey3 = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||||
|
node3 = WakuNode.new(nodeKey3, bindIp, Port(60003), sendSignedPeerRecord = true)
|
||||||
|
|
||||||
|
var
|
||||||
|
peerExchangeHandler: RoutingRecordsHandler
|
||||||
|
completionFut = newFuture[bool]()
|
||||||
|
|
||||||
|
proc handlePeerExchange(peer: PeerId, topic: string,
|
||||||
|
peers: seq[RoutingRecordsPair]) {.gcsafe, raises: [Defect].} =
|
||||||
|
## Handle peers received via gossipsub peer exchange
|
||||||
|
let peerRecords = peers.mapIt(it.record.get())
|
||||||
|
|
||||||
|
check:
|
||||||
|
# Node 3 is informed of node 2 via peer exchange
|
||||||
|
peer == node1.switch.peerInfo.peerId
|
||||||
|
topic == defaultTopic
|
||||||
|
peerRecords.countIt(it.peerId == node2.switch.peerInfo.peerId) == 1
|
||||||
|
|
||||||
|
if (not completionFut.completed()):
|
||||||
|
completionFut.complete(true)
|
||||||
|
|
||||||
|
peerExchangeHandler = handlePeerExchange
|
||||||
|
|
||||||
|
node1.mountRelay()
|
||||||
|
node2.mountRelay()
|
||||||
|
node3.mountRelay(peerExchangeHandler = some(peerExchangeHandler))
|
||||||
|
|
||||||
|
# Ensure that node1 prunes all peers after the first connection
|
||||||
|
node1.wakuRelay.parameters.dHigh = 1
|
||||||
|
|
||||||
|
await allFutures([node1.start(), node2.start(), node3.start()])
|
||||||
|
|
||||||
|
await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()])
|
||||||
|
|
||||||
|
await node3.connectToNodes(@[node1.switch.peerInfo.toRemotePeerInfo()])
|
||||||
|
|
||||||
|
check:
|
||||||
|
(await completionFut.withTimeout(5.seconds)) == true
|
||||||
|
|
||||||
|
await allFutures([node1.stop(), node2.stop(), node3.stop()])
|
|
@ -89,6 +89,11 @@ type
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
name: "relay" }: bool
|
name: "relay" }: bool
|
||||||
|
|
||||||
|
relayPeerExchange* {.
|
||||||
|
desc: "Enable gossipsub peer exchange in relay protocol: true|false",
|
||||||
|
defaultValue: true
|
||||||
|
name: "relay-peer-exchange" }: bool
|
||||||
|
|
||||||
rlnRelay* {.
|
rlnRelay* {.
|
||||||
desc: "Enable spam protection through rln-relay: true|false",
|
desc: "Enable spam protection through rln-relay: true|false",
|
||||||
defaultValue: false
|
defaultValue: false
|
||||||
|
|
|
@ -53,7 +53,7 @@ logScope:
|
||||||
const clientId* = "Nimbus Waku v2 node"
|
const clientId* = "Nimbus Waku v2 node"
|
||||||
|
|
||||||
# Default topic
|
# Default topic
|
||||||
const defaultTopic = "/waku/2/default-waku/proto"
|
const defaultTopic* = "/waku/2/default-waku/proto"
|
||||||
|
|
||||||
# Default Waku Filter Timeout
|
# Default Waku Filter Timeout
|
||||||
const WakuFilterTimeout: Duration = 1.days
|
const WakuFilterTimeout: Duration = 1.days
|
||||||
|
@ -170,6 +170,7 @@ proc new*(T: type WakuNode, nodeKey: crypto.PrivateKey,
|
||||||
secureCert: string = "",
|
secureCert: string = "",
|
||||||
wakuFlags = none(WakuEnrBitfield),
|
wakuFlags = none(WakuEnrBitfield),
|
||||||
nameResolver: NameResolver = nil,
|
nameResolver: NameResolver = nil,
|
||||||
|
sendSignedPeerRecord = false,
|
||||||
dns4DomainName = none(string)
|
dns4DomainName = none(string)
|
||||||
): T
|
): T
|
||||||
{.raises: [Defect, LPError, IOError, TLSStreamProtocolError].} =
|
{.raises: [Defect, LPError, IOError, TLSStreamProtocolError].} =
|
||||||
|
@ -242,7 +243,8 @@ proc new*(T: type WakuNode, nodeKey: crypto.PrivateKey,
|
||||||
wssEnabled = wssEnabled,
|
wssEnabled = wssEnabled,
|
||||||
secureKeyPath = secureKey,
|
secureKeyPath = secureKey,
|
||||||
secureCertPath = secureCert,
|
secureCertPath = secureCert,
|
||||||
nameResolver = nameResolver)
|
nameResolver = nameResolver,
|
||||||
|
sendSignedPeerRecord = sendSignedPeerRecord)
|
||||||
|
|
||||||
let wakuNode = WakuNode(
|
let wakuNode = WakuNode(
|
||||||
peerManager: PeerManager.new(switch, peerStorage),
|
peerManager: PeerManager.new(switch, peerStorage),
|
||||||
|
@ -673,7 +675,8 @@ proc startRelay*(node: WakuNode) {.async.} =
|
||||||
proc mountRelay*(node: WakuNode,
|
proc mountRelay*(node: WakuNode,
|
||||||
topics: seq[string] = newSeq[string](),
|
topics: seq[string] = newSeq[string](),
|
||||||
relayMessages = true,
|
relayMessages = true,
|
||||||
triggerSelf = true)
|
triggerSelf = true,
|
||||||
|
peerExchangeHandler = none(RoutingRecordsHandler))
|
||||||
# @TODO: Better error handling: CatchableError is raised by `waitFor`
|
# @TODO: Better error handling: CatchableError is raised by `waitFor`
|
||||||
{.gcsafe, raises: [Defect, InitializationError, LPError, CatchableError].} =
|
{.gcsafe, raises: [Defect, InitializationError, LPError, CatchableError].} =
|
||||||
|
|
||||||
|
@ -699,6 +702,10 @@ proc mountRelay*(node: WakuNode,
|
||||||
## all configured topics plus the hard-coded defaultTopic(s)
|
## all configured topics plus the hard-coded defaultTopic(s)
|
||||||
wakuRelay.defaultTopics = concat(@[defaultTopic], topics)
|
wakuRelay.defaultTopics = concat(@[defaultTopic], topics)
|
||||||
|
|
||||||
|
## Add peer exchange handler
|
||||||
|
if peerExchangeHandler.isSome():
|
||||||
|
wakuRelay.routingRecordsHandler.add(peerExchangeHandler.get())
|
||||||
|
|
||||||
node.switch.mount(wakuRelay, protocolMatcher(WakuRelayCodec))
|
node.switch.mount(wakuRelay, protocolMatcher(WakuRelayCodec))
|
||||||
|
|
||||||
if relayMessages:
|
if relayMessages:
|
||||||
|
@ -1107,6 +1114,7 @@ when isMainModule:
|
||||||
conf.websocketSecureCertPath,
|
conf.websocketSecureCertPath,
|
||||||
some(wakuFlags),
|
some(wakuFlags),
|
||||||
dnsResolver,
|
dnsResolver,
|
||||||
|
conf.relayPeerExchange, # We send our own signed peer record when peer exchange enabled
|
||||||
dns4DomainName
|
dns4DomainName
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1147,7 +1155,7 @@ when isMainModule:
|
||||||
ok(node)
|
ok(node)
|
||||||
|
|
||||||
# 4/7 Mount and initialize configured protocols
|
# 4/7 Mount and initialize configured protocols
|
||||||
proc setupProtocols(node: var WakuNode,
|
proc setupProtocols(node: WakuNode,
|
||||||
conf: WakuNodeConf,
|
conf: WakuNodeConf,
|
||||||
mStorage: WakuMessageStore = nil): SetupResult[bool] =
|
mStorage: WakuMessageStore = nil): SetupResult[bool] =
|
||||||
|
|
||||||
|
@ -1156,10 +1164,26 @@ when isMainModule:
|
||||||
## No protocols are started yet.
|
## No protocols are started yet.
|
||||||
|
|
||||||
# Mount relay on all nodes
|
# Mount relay on all nodes
|
||||||
|
var peerExchangeHandler = none(RoutingRecordsHandler)
|
||||||
|
if conf.relayPeerExchange:
|
||||||
|
proc handlePeerExchange(peer: PeerId, topic: string,
|
||||||
|
peers: seq[RoutingRecordsPair]) {.gcsafe, raises: [Defect].} =
|
||||||
|
## Handle peers received via gossipsub peer exchange
|
||||||
|
# TODO: Only consider peers on pubsub topics we subscribe to
|
||||||
|
let exchangedPeers = peers.filterIt(it.record.isSome()) # only peers with populated records
|
||||||
|
.mapIt(toRemotePeerInfo(it.record.get()))
|
||||||
|
|
||||||
|
debug "connecting to exchanged peers", src=peer, topic=topic, numPeers=exchangedPeers.len
|
||||||
|
|
||||||
|
# asyncSpawn, as we don't want to block here
|
||||||
|
asyncSpawn node.connectToNodes(exchangedPeers, "peer exchange")
|
||||||
|
|
||||||
|
peerExchangeHandler = some(handlePeerExchange)
|
||||||
|
|
||||||
mountRelay(node,
|
mountRelay(node,
|
||||||
conf.topics.split(" "),
|
conf.topics.split(" "),
|
||||||
relayMessages = conf.relay, # Indicates if node is capable to relay messages
|
relayMessages = conf.relay, # Indicates if node is capable to relay messages
|
||||||
)
|
peerExchangeHandler = peerExchangeHandler)
|
||||||
|
|
||||||
# Keepalive mounted on all nodes
|
# Keepalive mounted on all nodes
|
||||||
mountLibp2pPing(node)
|
mountLibp2pPing(node)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Collection of utilities related to Waku peers
|
# Collection of utilities related to Waku peers
|
||||||
import
|
import
|
||||||
std/[options, strutils],
|
std/[options, sequtils, strutils],
|
||||||
stew/results,
|
stew/results,
|
||||||
stew/shims/net,
|
stew/shims/net,
|
||||||
eth/keys,
|
eth/keys,
|
||||||
|
@ -11,7 +11,8 @@ import
|
||||||
libp2p/[errors,
|
libp2p/[errors,
|
||||||
multiaddress,
|
multiaddress,
|
||||||
peerid,
|
peerid,
|
||||||
peerinfo]
|
peerinfo,
|
||||||
|
routing_record]
|
||||||
|
|
||||||
type
|
type
|
||||||
RemotePeerInfo* = ref object of RootObj
|
RemotePeerInfo* = ref object of RootObj
|
||||||
|
@ -152,6 +153,12 @@ proc toRemotePeerInfo*(enr: enr.Record): Result[RemotePeerInfo, cstring] =
|
||||||
|
|
||||||
return ok(RemotePeerInfo.init(peerId, addrs, some(enr)))
|
return ok(RemotePeerInfo.init(peerId, addrs, some(enr)))
|
||||||
|
|
||||||
|
## Converts peer records to dialable RemotePeerInfo
|
||||||
|
## Useful if signed peer records have been received in an exchange
|
||||||
|
proc toRemotePeerInfo*(peerRecord: PeerRecord): RemotePeerInfo =
|
||||||
|
RemotePeerInfo.init(peerRecord.peerId,
|
||||||
|
peerRecord.addresses.mapIt(it.address))
|
||||||
|
|
||||||
## Converts the local peerInfo to dialable RemotePeerInfo
|
## Converts the local peerInfo to dialable RemotePeerInfo
|
||||||
## Useful for testing or internal connections
|
## Useful for testing or internal connections
|
||||||
proc toRemotePeerInfo*(peerInfo: PeerInfo): RemotePeerInfo =
|
proc toRemotePeerInfo*(peerInfo: PeerInfo): RemotePeerInfo =
|
||||||
|
|
|
@ -61,6 +61,7 @@ proc newWakuSwitch*(
|
||||||
maxOut = -1,
|
maxOut = -1,
|
||||||
maxConnsPerPeer = MaxConnectionsPerPeer,
|
maxConnsPerPeer = MaxConnectionsPerPeer,
|
||||||
nameResolver: NameResolver = nil,
|
nameResolver: NameResolver = nil,
|
||||||
|
sendSignedPeerRecord = false,
|
||||||
wssEnabled: bool = false,
|
wssEnabled: bool = false,
|
||||||
secureKeyPath: string = "",
|
secureKeyPath: string = "",
|
||||||
secureCertPath: string = ""): Switch
|
secureCertPath: string = ""): Switch
|
||||||
|
@ -77,6 +78,8 @@ proc newWakuSwitch*(
|
||||||
.withNoise()
|
.withNoise()
|
||||||
.withTcpTransport(transportFlags)
|
.withTcpTransport(transportFlags)
|
||||||
.withNameResolver(nameResolver)
|
.withNameResolver(nameResolver)
|
||||||
|
.withSignedPeerRecord(sendSignedPeerRecord)
|
||||||
|
|
||||||
if privKey.isSome():
|
if privKey.isSome():
|
||||||
b = b.withPrivateKey(privKey.get())
|
b = b.withPrivateKey(privKey.get())
|
||||||
if wsAddress.isSome():
|
if wsAddress.isSome():
|
||||||
|
|
Loading…
Reference in New Issue