Run a delivery node#

Runs a delivery node (a Logos Core daemon + delivery_module 0.4.0, for Logos Testnet v0.3). There is no GUI or HTTP API — interaction is via the daemon’s CLI (logosctl, or logoscore in the Nix setup). You can run it three ways:

All three connect the node to the logos.test fleet by default. A node that other peers should be able to dial also needs its public address set.

With Docker#

Prerequisites#

  • Docker with Compose

Start#

git clone https://github.com/logos-co/logos-delivery-module.git
cd logos-delivery-module
docker compose up -d --build

The image is built from logos-docker: the logosctl daemon with delivery_module 0.4.0 and the RLN modules (liblogos_rln_module 0.12.0, liblogos_lez_rln_module 5.1.0) from the Logos catalog.

Boot the node#

The daemon is running; load the module and create the node:

docker exec logos-node logosctl module load delivery_module
docker exec logos-node logosctl call delivery_module createNode @/conf/logos-test.json

logos.test runs RLN, so the node needs a membership before it can start — fund it, then:

docker exec logos-node logosctl call delivery_module start

Verify:

docker exec logos-node logosctl daemon status

The daemon logs to the container’s output: docker logs logos-node.

Stop#

docker compose down

Node data lives in the logos-persistence volume, so it survives down and rebuilds. docker compose down -v removes it.

Without Docker: prebuilt binaries#

Run a node from released binaries — nothing to build, no repository clone. You need one CLI, logosctl 0.3.0: the node daemon, its client, and the package manager in one binary.

Install logosctl#

Download the archive for your system from the logosctl 0.3.0 release and follow its install steps to put logosctl on your PATH.

Install the module and boot the node#

# Start the daemon; --detach returns once it accepts commands
logosctl daemon start --detach

# Fetch delivery_module and the RLN modules logos.test needs from the Logos
# catalog and install them (liblogos_lez_rln_module comes in as a dependency)
logosctl catalog refresh
logosctl package install delivery_module --version 0.4.0 --yes
logosctl package install liblogos_rln_module --version 0.12.0 --yes

# logos.test node config (layered createNode shape — see Configuration below)
cat > logos-test.json <<'JSON'
{
  "preset": "logos.test",
  "messagingOverrides": {
    "logLevel": "DEBUG",
    "tcp-port": 30303,
    "discv5-udp-port": 9000,
    "nat": "extip:<public-ip>"
  }
}
JSON

logosctl module load delivery_module
logosctl call delivery_module createNode @logos-test.json
# fund the node's RLN membership first (see below), then:
logosctl call delivery_module start

Replace <public-ip> with the host’s public IPv4 address, or drop the nat line for a node nobody needs to dial — see Public address.

logosctl keeps everything — installed modules, logs, node data — in its session directory, ~/.logosctl (--config-dir picks another). The daemon log is ~/.logosctl/logs/daemon.log.

Verify with logosctl daemon status; stop with logosctl daemon stop.

Fund the node’s RLN membership#

logos.test rate-limits with RLN on the LEZ testnet (see rln.md). createNode brings the RLN modules up on the preset’s registry; they create the node’s own wallet and register a membership as soon as its payer account holds enough native LEZ — the node pays for itself, nothing is registered by hand. start passes only once that membership is active.

With Docker, prefix each command with docker exec logos-node.

# The account to fund (state "ready", "network":"testnet")
logosctl call liblogos_lez_rln_module wallet_status
# → {... "payer":"<ACCOUNT>" ...}

Send it at least 200000000 native on the LEZ testnet: the registration price is 1000000 (rate limit 100), and the transaction’s fee reserve, sized from the chain’s live fees, is ~1.8e8 at base fee 8 (most of it is refunded). Bridge funds from the Logos blockchain testnet with a channel deposit, or transfer from an account that already holds LEZ testnet native.

Then wait for the membership (typically 1–3 minutes after the funds land):

printf '%s' '5e269b6a19fce081f5808b13442dcbc3522197638dd38df5a28bc4e55236b977' > /tmp/rln-id.arg
logosctl call liblogos_rln_module get_membership_state \
  logos:testnet:fd322d32a46ddf57eece9db8d68e9b722262d1f770760fca63b8cb9e7775b971 @/tmp/rln-id.arg
# "state":"unknown" with provisioning.step "awaiting_funding" → not funded yet
# "state":"active"                                            → ready to start

The wallet (and the key to its funds) lives in the node’s data — the logos-persistence volume with Docker, the session directory otherwise.

Without Docker: build with Nix#

Build the runtime and this module with Nix, then run the logoscore daemon directly on the host.

Prerequisites#

  • Nix with flakes enabled

  • Linux or macOS

Build the runtime and module#

Build the logoscore CLI (the headless runtime) and the lgpm package manager from their flakes, then build and install this module’s .lgx. Both of its dependencies are optional. libp2p_module hosts external service discovery: a node configured for it (plugin-kad-discovery) fails to start without it, any other node runs without it. An RLN-enabled preset also needs liblogos_rln_module and liblogos_lez_rln_module. This flake re-exports their .lgxs from its own locked inputs, so they match the revs the module was built against:

git clone https://github.com/logos-co/logos-delivery-module.git
cd logos-delivery-module
git checkout v0.4.0

# Runtime + package manager
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
nix build 'github:logos-co/logos-package-manager#cli' -o lgpm

# This module, built from the checkout
nix build '.#lgx' -o delivery-lgx

# libp2p, for external service discovery
nix build '.#libp2p_module-lgx' -o libp2p-lgx

# Its RLN dependency chain
nix build '.#liblogos_rln_module-lgx' -o rln-lgx
nix build '.#liblogos_lez_rln_module-lgx' -o lez-rln-lgx

# Seed the modules dir with the bundled capability module, then install
mkdir -p modules
cp -RL ./logos/modules/. ./modules/
for pkg in libp2p-lgx lez-rln-lgx rln-lgx delivery-lgx; do
  ./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file "$pkg"/*.lgx
done

The first build compiles the whole runtime stack through Nix — allow ~30–45 min. Later builds are fast.

.#lgx is a development package that only the Nix-built runtime above can load. To install a local build into a released logosctl instead, build .#lgx-portable and pass it to logosctl package install.

Start the daemon#

Put logoscore on your PATH and start it in daemon mode pointed at ./modules:

export PATH="$PWD/logos/bin:$PATH"
logoscore -D -m ./modules > logs.txt &

Boot the node#

logoscore load-module delivery_module
logoscore call delivery_module createNode @conf/logos-test.json
logoscore call delivery_module start

Verify:

logoscore status

Stop#

logoscore stop

For a fully pinned, build-from-this-commit walkthrough — plus notes on the blocking Kademlia bootstrap in headless runs — see the runtime doc-test.

Check the node#

Once start returns, the node joins the network in the background. Within a few minutes it should report connected peers:

logosctl call delivery_module getNodeInfo Metrics --json \
  | jq -r .result.value | grep '^libp2p_peers '
# libp2p_peers 8.0

and advertise its public address in its ENR and listen addresses:

logosctl call delivery_module getNodeInfo MyMultiaddresses --json | jq -r .result.value
# /ip4/<public-ip>/tcp/30303/p2p/16Uiu2…,/ip4/<public-ip>/udp/30303/quic-v1/p2p/16Uiu2…

With Docker, prefix them with docker exec logos-node; with the Nix build, use logoscore call …. See query-node.md for everything else the node reports.

Configuration#

The config uses the layered createNode shape: preset picks the network, mode defaults to "Core" ("Edge" for a light node), per-layer settings go in messagingOverrides. The repo ships it as conf/logos-test.json: Docker mounts it into the container at /conf (@/conf/logos-test.json); with the Nix build, pass the path directly (@conf/logos-test.json). The prebuilt-binaries path above writes the same config inline, plus nat. Edit it and re-run the boot steps to change settings.

Keep extra keys inside messagingOverrides / channelsOverrides / kernelConf — a bare top-level key (even logLevel) switches parsing to the legacy flat shape. Unpinned listening ports are OS-assigned; the config pins the p2p ports to match the Docker port mappings.

For the dev network, use conf/logos-dev.json (preset logos.dev) — see networks.md for how the two differ. The full config grammar, including kernel-only nodes ("entryLayer": "kernel"), is documented in the API reference.

The node’s local state goes to the runtime’s per-instance persistence directory unless localStoragePath names another.

Public address#

The node advertises only addresses peers can actually dial. It no longer publishes 0.0.0.0 or a placeholder port: until it learns its public address, its ENR carries no IP at all and other nodes cannot connect to it.

For a node others should reach, set the address explicitly:

"messagingOverrides": {
  "nat": "extip:<public-ip>"
}

nat also takes "any" (the default: discover a UPnP or NAT-PMP gateway), "upnp", "pmp" or "none". Inside Docker, use extip. Open the p2p port (tcp-port, 30303) for both TCP and UDP (QUIC, below) and the UDP discovery port (discv5-udp-port, 9000) on the host firewall.

QUIC#

QUIC is on by default and runs next to TCP. Unless quic-port names another port, it listens on UDP at the TCP port number — /udp/30303/quic-v1 for the config above. docker-compose.yml already publishes it.

On Linux, raise the socket receive buffer limit, or the node logs QUIC UDP receive buffer capped below requested size at every start:

sudo sysctl -w net.core.rmem_max=8388608

RLN#

RLN comes from the preset — there is nothing to configure. logos.test runs with it on (the LEZ testnet registry), so a logos.test node needs the RLN modules and an active RLN membership; see rln.md. logos.dev runs with it off, so a logos.dev node needs no RLN modules; module load reports liblogos_rln_module as skipped and carries on. networks.md tracks each network’s RLN state.

On a preset with RLN on, install the RLN modules next to this one before createNode, or bring-up ends in Failed:

  • Prebuilt binaries — they are in a separate catalog:

    logosctl catalog add https://github.com/logos-co/logos-rln-modules/releases/download/index/logos-repo.json
    logosctl catalog refresh
    logosctl package install liblogos_rln_module --yes
    
  • Nix — the build above already installs them.

  • Docker — set the RLN_VERSION build arg of the logos-docker image in docker-compose.yml, with MODULES_REPO pointing at a catalog that carries the RLN modules.

Follow bring-up with rlnState: Disabled → Initializing → Ready | Failed. See rln.md for the details.

Plugin-hosted discovery#

Enable it with "pluginKadDiscovery": true in messagingOverrides (or in kernelConf for a kernel-only node); leave it out to keep the node’s internal discovery. On top of conf/logos-dev.json the switch is all it takes:

{
  "preset": "logos.dev",
  "messagingOverrides": { "pluginKadDiscovery": true }
}

Extra DHT bootstrap peers go through the node’s own kad-bootstrap-node key.

It needs libp2p_module installed next to this module (see the Nix build above). Without it start fails and the node is stopped. On Windows, where libp2p_module has no build, use internal discovery.

LIBP2P_MODULE_CONFIG (inline JSON or a file path) is optional. Set it only to override libp2p’s own defaults, for example its listen addresses when other nodes must reach it: unset, it listens on an ephemeral loopback port.

How it works is described in Architecture.

Metrics#

The node already aggregates Prometheus metrics internally (the same set exposed on metricsServerPort, rendered behind the "Metrics" node-info attribute). collectOpenMetricsText() hands that exposition text back verbatim so the openmetrics module can scrape this module without standing up a separate HTTP server — no in-module parsing or reshaping. The openmetrics scraper parses the text, injects a module="delivery_module" label on every series, and merges it with other modules.

Point the openmetrics module at this one by name, selecting the text-source convention with "format": "text":

logoscore --config-dir /tmp/om call openmetrics start \
  '{"port":9090,"modules":[{"name":"delivery_module","format":"text"}]}'
curl http://localhost:9090/metrics   # every series carries module="delivery_module"

Before a node is created (or if the read fails) collectOpenMetricsText() returns an empty document so a scrape never errors out on this module.

For a one-off read without the openmetrics module, the raw exposition text is also available via getNodeInfo Metrics — see query-node.md.