Files

9.5 KiB

logos-libp2p-module

logos-libp2p-module is a Logos module that integrates libp2p networking capabilities (via nim-libp2p C bindings) into the Logos ecosystem.

It provides:

  • Peer connectivity
  • Stream management
  • Kademlia DHT operations
  • Gossipsub operations
  • Sync and async APIs compatible with Qt

For guided walkthroughs and complete usage demonstrations, see the tutorials.

See how other projects are using logos-libp2p-module:

  • Demo Chat is a standalone terminal application that demonstrates how to build a chat experience with GossipSub.
  • To add another project to this list, open a pull request with a link and a short description.

Configuration

When the module is loaded by logoscore, it is constructed without arguments, so options are supplied through the LIBP2P_MODULE_CONFIG environment variable — set to either inline JSON or a path to a JSON file. The config is overlaid onto the defaults at load time; every key is optional, and an omitted key keeps its default. If the config is unset, unreadable, not valid JSON, or has a wrong-typed field, the module logs a warning and falls back to the full default options.

# inline
export LIBP2P_MODULE_CONFIG='{
  "addrs": ["/ip4/0.0.0.0/tcp/9000"],
  "bootstrapNodes": [
    { "peerId": "16Uiu2...", "addrs": ["/ip4/1.2.3.4/tcp/9000"] }
  ],
  "transport": "tcp",
  "privKey": "08021220..."
}'

# or a file
export LIBP2P_MODULE_CONFIG=/etc/logos/libp2p.json

See config.example.json for a ready-to-edit sample and the config section of metadata.json for the full schema. Code that constructs Libp2pModuleImpl directly (tutorials, tests) passes Libp2pModuleOptions to the constructor and bypasses this path.

GossipSub queue bounds

Delivered messages wait in a per-topic queue until gossipsubNextMessage pops them. Both bounds apply together, because 1024 messages at the 1 MiB GossipSub message limit is still 1 GiB per topic.

Key Default Meaning
gossipsubQueueMaxMessages 1024 Messages held per topic. 0 disables the queue.
gossipsubQueueMaxBytes 4194304 Bytes held per topic. 0 disables the queue.

Past either bound the newest message is dropped and counted in libp2p_module_gossipsub_queue_dropped_total, reported per topic by collectMetrics alongside libp2p_module_gossipsub_queue_depth. The byte bound holds on an empty queue too, so keep gossipsubQueueMaxBytes above gossipsubMaxMessageSize: a larger message never fits and is always dropped. Set gossipsubQueueMaxBytes to 0 if your application reads only the gossipsubMessage event. gossipsubMaxMessageSize below raises the per-message ceiling, so raise it and the queue bounds together.

GossipSub ingress limits

The queue bounds hold what a peer already delivered. These keys bound what a peer can deliver, and nim-libp2p applies them inside GossipSub.

Key Default Meaning
gossipsubMaxMessageSize 0 Largest message accepted or sent, in bytes. 0 keeps the core 1 MiB limit; the ceiling is MAX_GOSSIPSUB_MESSAGE_SIZE.
gossipsubOverheadRateLimitBytes 0 Per-peer budget of protocol-overhead bytes per interval. 0 disables the limit.
gossipsubOverheadRateLimitIntervalMs 0 Refill interval of that budget, up to MAX_OVERHEAD_RATE_LIMIT_INTERVAL_MS.
gossipsubDisconnectPeerAboveRateLimit false Disconnect a peer that spends its budget.

A rate limit needs both gossipsubOverheadRateLimitBytes and gossipsubOverheadRateLimitIntervalMs, and every key here needs mountGossipsub. A broken combination fails node creation with the reason, so a node never starts with a limit that does nothing. While gossipsubDisconnectPeerAboveRateLimit is false, an empty budget only increments libp2p_gossipsub_peers_rate_limit_hits; set the flag to enforce it.


Running a node via logoscore

The module can be driven directly from logoscore without any other module. A default node is created when the module is loaded; createNode then rebuilds it from a call-time config (same schema as LIBP2P_MODULE_CONFIG, so @config.json expands to the file's contents), and getNodeInfo reads back node details.

First build the module's .lgx bundle, install it into a modules directory, and start the daemon against that directory:

nix build '.#lgx'                       # result/ holds the .lgx bundle
lgpm --modules-dir ./modules --allow-unsigned install --file result/*.lgx
lgpm --modules-dir ./modules list       # confirm "libp2p_module" is listed

logoscore -D -m ./modules &             # start the daemon against ./modules

The daemon binds its socket asynchronously, so the first load-module can race it — retry once if it reports an RPC failure. Then drive the node:

logoscore load-module libp2p_module
logoscore call libp2p_module createNode @config.example.json   # or inline JSON
logoscore call libp2p_module start
logoscore call libp2p_module getNodeInfo Version        # module version
logoscore call libp2p_module getNodeInfo MyBoundPorts   # bound ports, e.g. [9000]
logoscore call libp2p_module getNodeInfo PeerId         # this node's peer id
logoscore call libp2p_module getNodeInfo Multiaddrs     # full bound multiaddrs
logoscore stop

createNode is optional: if you set LIBP2P_MODULE_CONFIG before loading, the node is already configured and you can start straight away. Calling createNode tears down the existing node and builds a fresh one from the supplied config, so issue it before start. It accepts inline JSON or @config.json (the file's contents); wrap inline JSON in single quotes so the shell doesn't mangle it.

logoscore only relays a generic "call failed" to the CLI; the specific reason (createNode: invalid config: …, libp2p_new failed: …) is written to the daemon's stderr, so check the daemon output (or its redirected log) when a call fails.

A scripted version of this flow runs in CI and locally via nix run .#standalone-e2e (see tests/README.md).


Custom-protocol bridge

Another module can run its own length-prefixed protocol on this node, instead of starting a second libp2p node. Every argument and every result is one JSON string, and every payload is base64, because that is the only shape the universal codegen marshals.

Call Args Result
protocolRequest {peerId, proto, multiaddrs?, requestB64, timeoutMs?, maxSize?, expectResponse?} {responseB64}
mountProtocol proto (none)
protocolAcceptStream {proto, timeoutMs?} {streamId, proto, peerId}
streamReadLpJson {streamId, maxSize?, timeoutMs?} {dataB64}
streamWriteLpJson {streamId, dataB64} (none)
streamCloseJson / streamReleaseJson {streamId} (none)
pingPeer peerId, timeoutMs {peerId, rttMs}

protocolRequest does connect, dial, write, read and release in one call, so it covers a request and response exchange.

The module boundary carries no push events, so an inbound stream waits in a per-protocol queue and the consumer polls it with protocolAcceptStream. peerId is the peer that opened the stream, which a service protocol needs to answer the right peer. A stream leaves the queue when protocolAcceptStream hands it out, when streamReleaseJson releases it, or when the node stops. Mounting needs no event listener, so a consumer that only polls never sets one.

Each inbound stream also fires a protocolStream event with the same {streamId, proto, peerId}. Drive a stream from the queue or from the event, never from both: the two report the same stream, and the second reader finds a handle another one already released.

The queue holds 1024 streams per protocol. Past that the newest inbound stream is released, and the drop is counted, so a consumer that stops polling loses streams instead of pinning them:

Metric Type Labels
libp2p_module_protocol_stream_queue_depth gauge proto
libp2p_module_protocol_stream_dropped_total counter proto

pingPeer dials /ipfs/ping/1.0.0 on a peer this node is already connected to. Use it for a health check; it opens no connection of its own. rttMs is measured around the write and the read, so it carries the two FFI hops on top of the wire time. Compare it against itself over time, not against ping(8).


Building

Currently the recommended and supported building way is using Nix

Build everything (default)

nix build

Or explicitly

nix build '.#default'

The result will include:

  • /lib/libp2p_module_plugin.so (or .dylib on macOS) — The Logos libp2p module plugin
  • /include/libp2p_module_api.h — Generated module API header
  • /include/libp2p_module_api.cpp — Generated module API implementation

Build Individual Components

Build only the library (plugin)

nix build '.#lib'

Build only the generated headers

nix build '.#include'

Development Shell

Enter development environment

nix develop

This provides:

  • CMake
  • Ninja
  • Qt6
  • Logos SDK dependencies
  • Proper environment variables

If flakes are not enabled globally:

nix build --extra-experimental-features 'nix-command flakes'

To enable globally, add flake as a experimental-feature to ~/.config/nix/nix.conf:

echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf