Skip to content

[Journey] Use the Service Discovery API from an app #307

@jm-clius

Description

@jm-clius

What the user achieves

A developer builds a Logos Core module that calls the libp2p_module Service Discovery API to advertise a named service to the network and discover other peers offering that same service.

Why it matters

Applications on the Logos network need a protocol-agnostic way to find peers offering specific services — mix nodes, relay nodes, storage providers — at runtime without hard-coding topology or peer lists. The Service Discovery API (LIP-160, built over Logos Service Discovery LIP-107 / Kad-DHT) enables any Logos Core module to perform typed, service-keyed peer lookups that work from lightweight client nodes that do not participate in DHT routing, unblocking any app that needs to wire itself into a live Logos network service.

Key components

  • logos-libp2p-module: A Logos Core module (type: core, interface: universal) wrapping nim-libp2p C bindings. Exposes all Service Discovery API methods (disco*) that the dependent module calls via LogosAPIClient.
  • Libp2pModuleOptions { .mountServiceDiscovery = true }: The struct field that activates the discovery subsystem at libp2p_module construction time; without it all disco* calls fail at runtime. Set in the libp2p_module's own config, not in the dependent module.
  • LogosModules / LogosAPIClient: The inter-module call interface injected into your module via setLogosModules. Use m_modules->api->getClient("libp2p_module") to obtain a client, then invokeRemoteMethod to make calls.
  • my_service_module: The new universal Logos Core module you write. Declares libp2p_module as a dependency, receives LogosModules* via setLogosModules, and wraps the disco* calls in typed methods.
  • examples/service_discovery.cpp: A 97-line self-contained two-node demo (direct Libp2pModuleImpl usage, not the Logos module pattern) that serves as a smoke test before building your own module.

Repository

https://github.com/logos-co/logos-libp2p-module

Runtime target

testnet v0.2

Prerequisites

  • OS: Linux (Ubuntu 22.04+) or macOS 14+
  • Hardware: 2 GB RAM sufficient for a local two-module test
  • Tools: Nix with flakes enabled (required); CMake ≥ 3.22, Ninja, Qt 6, GCC 12+/Clang 15+ are provided inside nix develop — do not install them separately

Commands and expected outputs

### Phase A — Smoke-test the example binary first

Build and run the bundled two-node demo to confirm the module is working before writing your own module.


git clone https://github.com/logos-co/logos-libp2p-module
cd logos-libp2p-module

# Build the module (first run fetches Nix deps — can take 5–20 min)
nix build -L

# Enter the dev shell, build examples
nix develop
cmake -B examples/build -S examples -DCMAKE_PREFIX_PATH="$(realpath result)"
cmake --build examples/build -j

./examples/build/service_discovery
# Starting nodes...
# Node A: starting advertising demo-service
# Node B: registering interest in demo-service
# Node B: looking up demo-service
# Node B found 1 peer(s) advertising demo-service
#   peer: 12D3KooW... seq: 1 addrs: 1
# Node A: random lookup
# Random lookup returned 1 peer(s)
# Node B: unregistering interest in demo-service
# Node A: stopping advertising demo-service
# Done


Note: peer IDs are non-deterministic

### Phase B — Create your Logos Core module


cd ..
nix run github:logos-co/logos-dev-boost -- init my_service_module --type module
# The scaffold tool prefixes the output directory with logos-:
cd logos-my-service-module


**`metadata.json`**declare the `libp2p_module` dependency:


{
  "name": "my_service_module",
  "version": "1.0.0",
  "description": "Service discovery demo module",
  "type": "core",
  "interface": "universal",
  "main": "my_service_module_plugin",
  "dependencies": ["libp2p_module"]
}


**`src/my_service_module_impl.h`**


#pragma once
#include <string>

struct LogosModules; // forward declaration — keeps header Qt-free

class MyServiceModuleImpl {
public:
    // The code generator detects setLogosModules by name and calls it in the
    // generated onInit. Do not declare onInit in this header — the generator
    // handles it entirely in the glue layer.
    void setLogosModules(LogosModules* m) { m_modules = m; }

    // serviceData: opaque bytes — any string is valid.
    // Recommended: Extensible Peer Record (LIP-74) for interoperability.
    // A plain string like "version=1" is sufficient for simple/internal use.
    std::string advertise(const std::string& serviceId, const std::string& serviceData);
    std::string discover(const std::string& serviceId);

private:
    LogosModules* m_modules = nullptr;
};


**`src/my_service_module_impl.cpp`**


#include "my_service_module_impl.h"
#include "logos_sdk.h"
#include "logos_api_client.h"
#include "logos_types.h"
#include <QString>
#include <QVariantList>
#include <thread>
#include <chrono>

std::string MyServiceModuleImpl::advertise(const std::string& serviceId,
                                           const std::string& serviceData) {
    if (!m_modules) return "error: not initialized";
    auto* client = m_modules->api->getClient("libp2p_module");

    // start() initialises the libp2p node; use a generous timeout
    auto qv = client->invokeRemoteMethod("libp2p_module", "start",
                                         QVariantList{}, Timeout(15000));
    auto r = qv.value<LogosResult>();
    if (!r.success) return "start failed: " + r.getError().toStdString();

    qv = client->invokeRemoteMethod("libp2p_module", "discoStart");
    r = qv.value<LogosResult>();
    if (!r.success) return "discoStart failed: " + r.getError().toStdString();

    qv = client->invokeRemoteMethod("libp2p_module", "discoStartAdvertising",
        QVariantList{QString::fromStdString(serviceId),
                     QString::fromStdString(serviceData)});
    r = qv.value<LogosResult>();
    if (!r.success) return "advertise failed: " + r.getError().toStdString();

    return "advertising " + serviceId;
}

std::string MyServiceModuleImpl::discover(const std::string& serviceId) {
    if (!m_modules) return "error: not initialized";
    auto* client = m_modules->api->getClient("libp2p_module");

    auto qv = client->invokeRemoteMethod("libp2p_module", "discoRegisterInterest",
        QVariantList{QString::fromStdString(serviceId)});
    auto r = qv.value<LogosResult>();
    if (!r.success) return "registerInterest failed: " + r.getError().toStdString();

    // DHT walk is async — allow time to propagate before querying
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    qv = client->invokeRemoteMethod("libp2p_module", "discoLookup",
        QVariantList{QString::fromStdString(serviceId), QString("")});
    r = qv.value<LogosResult>();
    if (!r.success) return "lookup failed: " + r.getError().toStdString();

    return r.value.toString().toStdString();
}


**`flake.nix`** — add `logos-libp2p-module` as a flake input:


inputs = {
  logos-module-builder.url = "github:logos-co/logos-module-builder";
  logos-libp2p-module.url  = "github:logos-co/logos-libp2p-module";
  # Local dev: override at build time without editing this file:
  # nix build --override-input logos-libp2p-module path:../logos-libp2p-module
};


### Phase C — Build both modules

The `.#install` target runs `lgpm` internally and produces the directory structure `logoscore` requires. Developers only write `metadata.json`; the install target generates `manifest.json`, `variant`, and co-locates all `.so` files automatically.


# In logos-my-service-module:
git init && git add -A
nix build .#install -L
# Produces: result/modules/my_service_module/
#   ├── manifest.json          (generated from metadata.json by lgpm)
#   ├── variant
#   └── my_service_module_plugin.so

# In logos-libp2p-module:
cd ../logos-libp2p-module
nix build .#install -L
# Produces: result/modules/libp2p_module/
#   ├── manifest.json
#   ├── variant
#   ├── libp2p_module_plugin.so
#   └── libp2p.so              (co-located automatically so $ORIGIN RUNPATH resolves)


### Phase D — Load both modules with logoscore

The `-m` flag can be repeated — each instance points at a `modules/` directory containing one or more module subdirectories.


cd ../logos-my-service-module

logoscore \
  -m ../logos-libp2p-module/result/modules \
  -m ./result/modules \
  -l libp2p_module,my_service_module \
  -c "my_service_module.advertise(logos/mix/v1, version=1)" \
  -c "my_service_module.discover(logos/mix/v1)" \
  --quit-on-finish
# advertising logos/mix/v1     ← from advertise()
# <peer record JSON>            ← from discover()


Note: `start()` requires bootstrap peers to complete. In a local environment without network peers, `advertise()` will time out at the logoscore client level — this is expected. The build and load are correct.

Success command

./examples/build/service_discovery

Expected result

Starting nodes...
Node A: starting advertising demo-service
Node B: registering interest in demo-service
Node B: looking up demo-service
Node B found 1 peer(s) advertising demo-service
  peer: 12D3KooW... seq: 1 addrs: 1
Node A: random lookup
Random lookup returned 1 peer(s)
Node B: unregistering interest in demo-service
Node A: stopping advertising demo-service
Done


Exit code 0. Peer IDs non-deterministic; count and field names stable. Any stderr or non-zero exit is a failure

Configuration details

Failure modes and limits

No response

GitHub handle

@gmelodie

Discord handle

gmelodie

Existing docs or specs

No response

Hardware requirements

Additional context

Repository: https://github.com/logos-co/logos-libp2p-module @ 5b1773ecbeda94e8b7ef72da99bb5fc14d0393e9
Canonical example: examples/service_discovery.cpp — 97-line self-contained two-node advertiser/discoverer demo
LIP-160 (Service Discovery API): logos-co/logos-lipsdocs/anoncomms/raw/service-discovery-api.md — defines disco* method signatures and Advertisement/AdvertisementList C types; editor: Simon-Pierre Vivier
LIP-107 (Logos Service Discovery Protocol): docs/anoncomms/raw/logos-service-discovery.md — defines Advertiser, Discoverer, Registrar roles and client/server mode distinction; editor: Arunima Chaudhuri
LIP-74 (Extensible Peer Records): docs/anoncomms/raw/extensible-peer-records.md — recommended protobuf encoding for serviceData; extends the standard libp2p peer record with a services field in a signed envelope; compliance is not required but strongly recommended for interoperability; editor: Hanno Cornelius

Estimated time to complete

15–25 minutes (Nix build dominates on first run; cached thereafter)

Security notes

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:journeyA user journey document (the primary deliverable).

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions