/// # Tutorial 3: Connecting Peers and Exchanging Data /// /// Now that we can create and configure nodes, let's make them talk to /// each other! In this tutorial we'll: /// /// - Create two nodes /// - Connect one to the other /// - Dial a protocol and open a stream /// - Exchange data over the stream /// /// ## How libp2p Connections Work /// /// A libp2p connection is established in two steps: /// /// 1. **Connect** — Establish a transport-level connection to a remote peer, /// identified by its Peer ID and a multiaddress. /// /// 2. **Dial** — Negotiate a protocol on top of the connection and open a /// bidirectional stream. The stream is what you actually read from and /// write to. /// /// Streams in `logos-libp2p-module` are identified by a numeric `streamId` /// that you get back from `dial()` and pass to read/write functions. /// /// ## Stream Lifecycle /// /// A stream must follow this lifecycle: /// 1. `dial()` — Open a stream (returns a `streamId`) /// 2. `streamWrite()` / `streamWriteLp()` — Send data /// 3. `streamReadLp()` / `streamReadExactly()` — Receive data /// 4. `streamClose()` or `streamCloseWithEOF()` — Close gracefully /// 5. `streamRelease()` — Free server-side resources /// /// ----------- #include #include #include #include #include "plugin.h" int main() { printf("=== Tutorial 3: Connecting Peers ===\n\n"); setLogLevel("fatal"); /// ## Step 1: Create and start two nodes /// /// We create two nodes. Node A listens on port 9190, Node B on port 9191. /// For simplicity, both mount the built-in `/ipfs/ping/1.0.0` protocol /// (enabled by default — no extra config needed). Libp2pModuleOptions optsA; optsA.addrs = {"/ip4/127.0.0.1/tcp/9190"}; Libp2pModuleOptions optsB; optsB.addrs = {"/ip4/127.0.0.1/tcp/9191"}; // Node B needs to know about node A to connect, but we'll pass // that info after starting both nodes. Libp2pModuleImpl nodeA(optsA); Libp2pModuleImpl nodeB(optsB); StdLogosResult startARes = nodeA.start(); if (!startARes.success) { fprintf(stderr, "Node A failed to start: %s\n", startARes.error.c_str()); return 1; } printf("Node A started\n"); StdLogosResult startBRes = nodeB.start(); if (!startBRes.success) { fprintf(stderr, "Node B failed to start: %s\n", startBRes.error.c_str()); return 1; } printf("Node B started\n"); /// ## Step 2: Get Node A's address info /// /// Node B needs to know where to find Node A. We get Node A's /// peer ID and listening addresses from `peerInfo()`. StdLogosResult infoA = nodeA.peerInfo(); if (!infoA.success) { fprintf(stderr, "Failed to get node A info: %s\n", infoA.error.c_str()); return 1; } std::string peerIdA = infoA.value["peerId"].get(); std::vector addrsA; for (const auto& a : infoA.value["addrs"]) { addrsA.push_back(a.get()); } printf("Node A peer ID: %s\n", peerIdA.c_str()); printf("Node A addresses:\n"); for (const auto& a : addrsA) { printf(" %s\n", a.c_str()); } /// ## Step 3: Connect Node B to Node A /// /// `connectPeer()` establishes the transport connection. The timeout /// parameter is in milliseconds. printf("\nConnecting Node B to Node A...\n"); StdLogosResult connectRes = nodeB.connectPeer(peerIdA, addrsA, 5000); if (!connectRes.success) { fprintf(stderr, "Failed to connect: %s\n", connectRes.error.c_str()); return 1; } printf("Connected!\n"); /// ## Step 4: List connected peers /// /// We can verify the connection by listing connected peers on each node. /// /// The direction parameter: /// - `PEER_DIRECTION_INBOUND` means "incoming connections" (other peers connected to this node). /// - `PEER_DIRECTION_OUTBOUND` means "outgoing connections" (this node connected to other peers). StdLogosResult peersA = nodeA.connectedPeers(PEER_DIRECTION_INBOUND); if (!peersA.success) { fprintf(stderr, "Failed to list Node A peers: %s\n", peersA.error.c_str()); return 1; } printf("\nNode A's incoming connected peers:\n"); for (const auto& p : peersA.value) { printf(" %s\n", p.get().c_str()); } StdLogosResult peersB = nodeB.connectedPeers(PEER_DIRECTION_OUTBOUND); if (!peersB.success) { fprintf(stderr, "Failed to list Node B peers: %s\n", peersB.error.c_str()); return 1; } printf("Node B's outgoing connected peers:\n"); for (const auto& p : peersB.value) { printf(" %s\n", p.get().c_str()); } /// ## Step 5: Dial the ping protocol and exchange data /// /// Now let's use the Ping protocol — a simple built-in protocol where /// the client sends a payload and the server echoes it back. /// /// `dial()` opens a stream on the remote peer for a specific protocol. /// It returns the `streamId` we use for subsequent operations. printf("\nDialing /ipfs/ping/1.0.0 on Node A...\n"); StdLogosResult dialRes = nodeB.dial(peerIdA, "/ipfs/ping/1.0.0"); if (!dialRes.success) { fprintf(stderr, "Dial failed: %s\n", dialRes.error.c_str()); return 1; } uint64_t streamId = dialRes.value.get(); printf("Stream opened, id: %llu\n", (unsigned long long)streamId); /// Write a 32-byte ping payload: std::string payload(32, '\0'); for (int i = 0; i < 32; ++i) { payload[i] = static_cast(i); } printf("Sending %zu bytes...\n", payload.size()); StdLogosResult writeRes = nodeB.streamWrite(streamId, payload); if (!writeRes.success) { fprintf(stderr, "Write failed: %s\n", writeRes.error.c_str()); return 1; } /// Read the echo (32 bytes back): StdLogosResult readRes = nodeB.streamReadExactly(streamId, 32); if (!readRes.success) { fprintf(stderr, "Read failed: %s\n", readRes.error.c_str()); return 1; } std::string reply = base64Decode(readRes.value.get()); /// Verify the echo matches: if (reply == payload) { printf("Ping successful — received matching echo back!\n"); } else { fprintf(stderr, "Ping payload mismatch\n"); return 1; } /// ## Step 6: Clean up the stream and nodes /// /// Always close and release streams when done: nodeB.streamClose(streamId); nodeB.streamRelease(streamId); nodeA.stop(); nodeB.stop(); printf("\n=== Tutorial 3 Complete ===\n"); return 0; } /// ## Run tutorial /// /// ```bash /// ./build/tutorial/tutorial_3_connecting_peers /// ```