diff --git a/examples/tutorial_5_discovery.nim b/examples/tutorial_5_discovery.nim new file mode 100644 index 000000000..ce02e19df --- /dev/null +++ b/examples/tutorial_5_discovery.nim @@ -0,0 +1,132 @@ +## # Discovery Manager +## +## In the [previous tutorial](tutorial_4_gossipsub.md), we built a custom protocol using [protobuf](https://developers.google.com/protocol-buffers) and +## spread informations (some metrics) on the network using gossipsub. +## For this tutorial, on the other hand, we'll go back on a simple example +## we'll try to discover a specific peers to greet on the network. +## +## First, as usual, we import the dependencies: +import sequtils +import chronos +import stew/byteutils + +import libp2p +import libp2p/protocols/rendezvous +import libp2p/discovery/rendezvousinterface +import libp2p/discovery/discoverymngr + +## We'll not use newStandardSwitch this time as we need the discovery protocol +## [RendezVous](https://github.com/libp2p/specs/blob/master/rendezvous/README.md) to be mounted on the switch using withRendezVous. +## +## Note that other discovery methods such as [Kademlia](https://github.com/libp2p/specs/blob/master/kad-dht/README.md) or [discv5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) exist. +proc createSwitch(rdv: RendezVous = RendezVous.new()): Switch = + SwitchBuilder.new() + .withRng(newRng()) + .withAddresses(@[ MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet() ]) + .withTcpTransport() + .withYamux() + .withNoise() + .withRendezVous(rdv) + .build() + +# Create a really simple protocol to log one message received then close the stream +const DumbCodec = "/dumb/proto/1.0.0" +type DumbProto = ref object of LPProtocol +proc new(T: typedesc[DumbProto], nodeNumber: int): T = + proc handle(conn: Connection, proto: string) {.async, gcsafe.} = + echo "Node", nodeNumber, " received: ", string.fromBytes(await conn.readLp(1024)) + await conn.close() + return T(codecs: @[DumbCodec], handler: handle) + +## ## Bootnodes +## The first time a p2p program is ran, he needs to know how to join +## its network. This is generally done by hard-coding a list of stable +## nodes in the binary, called "bootnodes". These bootnodes are a +## critical part of a p2p network, since they are used by every new +## user to onboard the network. +## +## By using libp2p, we can use any node supporting our discovery protocol +## (rendezvous in this case) as a bootnode. For this example, we'll +## create a bootnode, and then every peer will advertise itself on the +## bootnode, and use it to find other peers +proc main() {.async, gcsafe.} = + let bootNode = createSwitch() + await bootNode.start() + + # Create 5 nodes in the network + var + switches: seq[Switch] = @[] + discManagers: seq[DiscoveryManager] = @[] + + for i in 0..5: + let rdv = RendezVous.new() + # Create a remote future to await at the end of the program + let switch = createSwitch(rdv) + switch.mount(DumbProto.new(i)) + switches.add(switch) + + # A discovery manager is a simple tool, you can set it up by adding discovery + # interfaces (such as RendezVousInterface) then you can use it to advertise + # something on the network or to request something from it. + let dm = DiscoveryManager() + # A RendezVousInterface is a RendezVous protocol wrapped to be usable by the + # DiscoveryManager. + dm.add(RendezVousInterface.new(rdv)) + discManagers.add(dm) + + # We can now start the switch and connect to the bootnode + await switch.start() + await switch.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs) + + # Each nodes of the network will advertise on some topics (EvenGang or OddClub) + dm.advertise(RdvNamespace(if i mod 2 == 0: "EvenGang" else: "OddClub")) + + ## We can now create the newcomer. This peer will connect to the boot node, and use + ## it to discover peers & greet them. + let + rdv = RendezVous.new() + newcomer = createSwitch(rdv) + dm = DiscoveryManager() + await newcomer.start() + await newcomer.connect(bootNode.peerInfo.peerId, bootNode.peerInfo.addrs) + dm.add(RendezVousInterface.new(rdv, ttr = 250.milliseconds)) + + # Use the discovery manager to find peers on the OddClub topic to greet them + let queryOddClub = dm.request(RdvNamespace("OddClub")) + for _ in 0..2: + let + # getPeer give you a PeerAttribute containing informations about the peer. + res = await queryOddClub.getPeer() + # Here we will use the PeerId and the MultiAddress to greet him + conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec) + await conn.writeLp("Odd Club suuuucks! Even Gang is better!") + # Uh-oh! + await conn.close() + # Wait for the peer to close the stream + await conn.join() + # Queries will run in a loop, so we must stop them when we are done + queryOddClub.stop() + + # Maybe it was because he wanted to join the EvenGang + let queryEvenGang = dm.request(RdvNamespace("EvenGang")) + for _ in 0..2: + let + res = await queryEvenGang.getPeer() + conn = await newcomer.dial(res[PeerId], res.getAll(MultiAddress), DumbCodec) + await conn.writeLp("Even Gang is sooo laaame! Odd Club rocks!") + # Or maybe not... + await conn.close() + await conn.join() + queryEvenGang.stop() + # What can I say, some people just want to watch the world burn... Anyway + + # Stop all the discovery managers + for d in discManagers: + d.stop() + dm.stop() + + # Stop all the switches + await allFutures(switches.mapIt(it.stop())) + await allFutures(bootNode.stop(), newcomer.stop()) + +waitFor(main()) diff --git a/libp2p.nimble b/libp2p.nimble index 568df0046..1c5f40336 100644 --- a/libp2p.nimble +++ b/libp2p.nimble @@ -91,6 +91,7 @@ task website, "Build the website": tutorialToMd("examples/tutorial_2_customproto.nim") tutorialToMd("examples/tutorial_3_protobuf.nim") tutorialToMd("examples/tutorial_4_gossipsub.nim") + tutorialToMd("examples/tutorial_5_discovery.nim") tutorialToMd("examples/circuitrelay.nim") exec "mkdocs build" @@ -104,6 +105,7 @@ task examples_build, "Build the samples": # These tutorials relies on post 1.4 exception tracking buildSample("tutorial_3_protobuf", true) buildSample("tutorial_4_gossipsub", true) + buildSample("tutorial_5_discovery", true) # pin system # while nimble lockfile diff --git a/libp2p/protocols/rendezvous.nim b/libp2p/protocols/rendezvous.nim index 5460cdc21..1e7639511 100644 --- a/libp2p/protocols/rendezvous.nim +++ b/libp2p/protocols/rendezvous.nim @@ -559,7 +559,9 @@ proc request*(rdv: RendezVous, for (_, r) in s.values(): rdv.save(ns, peer, r, false) - for peer in rdv.peers: + # copy to avoid resizes during the loop + let peers = rdv.peers + for peer in peers: if limit == 0: break if RendezVousCodec notin rdv.switch.peerStore[ProtoBook][peer]: continue try: diff --git a/mkdocs.yml b/mkdocs.yml index c1906f3a3..10d8f05e5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,5 +46,6 @@ nav: - 'Create a custom protocol': tutorial_2_customproto.md - 'Protobuf': tutorial_3_protobuf.md - 'GossipSub': tutorial_4_gossipsub.md + - 'Discovery Manager': tutorial_5_discovery.md - 'Circuit Relay': circuitrelay.md - Reference: '/nim-libp2p/master/libp2p.html'