{"name":"Logos documentation","site":"https://docs.logos.co","generated_at":"2026-11-03T00:46:25.753Z","sections":[{"id":"get-started","title":"Get started","description":"What Logos is, the glossary, testnet endpoints, the map of repositories, and how to use these docs with a coding agent.","use_cases":["understand what Logos is and how the stack fits together","look up a Logos term (module, LGX, LEZ, zone, mixnet)","find testnet RPC endpoints, faucets and explorers","find which GitHub repository holds a component","connect a coding agent to these docs"],"llms_txt":"https://docs.logos.co/get-started/llms.txt","llms_full_txt":"https://docs.logos.co/get-started/llms-full.txt","pages":["/get-started/what-is-logos","/get-started/coding-with-agents","/get-started/logos-ecosystem-repositories","/get-started/glossary","/get-started/testnet-endpoints"]},{"id":"basecamp","title":"Basecamp","description":"Logos Basecamp, the local-first desktop launcher that installs and runs Logos modules and apps.","use_cases":["install Logos Basecamp","install and load an .lgx module into Basecamp","swap ETH and LEZ tokens in Basecamp"],"llms_txt":"https://docs.logos.co/basecamp/llms.txt","llms_full_txt":"https://docs.logos.co/basecamp/llms-full.txt","pages":["/basecamp/install-logos-basecamp","/basecamp/install-and-load-module-in-basecamp","/basecamp/atomic-swaps-poc"]},{"id":"build-an-app","title":"Build an app","description":"Logos Scaffold: the CLI that creates, builds and runs Logos modules and Basecamp apps, with its configuration reference and troubleshooting.","use_cases":["create a new Logos module or Basecamp app with logos-scaffold (lgs)","configure a scaffold project for Basecamp","debug a module that Basecamp does not load"],"llms_txt":"https://docs.logos.co/build-an-app/llms.txt","llms_full_txt":"https://docs.logos.co/build-an-app/llms-full.txt","pages":["/build-an-app/scaffold/about-logos-scaffold","/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold","/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp","/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference"]},{"id":"core","title":"Core","description":"Logos Core: building core and UI modules with logos-module-builder, metadata.json, the LGX package format, and the logosctl CLI.","use_cases":["build and run a Logos core module (C++/Qt plugin)","wrap an existing C library as a Logos module","build a C++ UI module for Basecamp","write or check a module metadata.json","package a module as .lgx (portable vs -dev variants)","run modules headlessly with logosctl"],"llms_txt":"https://docs.logos.co/core/llms.txt","llms_full_txt":"https://docs.logos.co/core/llms-full.txt","pages":["/core/build-modules/start-a-logos-module-from-the-cli","/core/build-modules/build-and-run-a-logos-core-module","/core/build-modules/wrap-a-c-library-as-a-logos-core-module","/core/build-modules/build-a-logos-cpp-ui-module","/core/reference/lgx-package-format-and-bundling-reference","/core/reference/logos-cli-reference"]},{"id":"blockchain","title":"Blockchain","description":"The Logos Blockchain (Cryptarchia consensus, Mantle, Blend network): running nodes, moving and bridging assets, zones, the Zone SDK and Logos SQL.","use_cases":["run a Logos Blockchain node from the CLI or from Basecamp","claim leader rewards","transfer tokens or bridge assets to a zone","build an application on a zone with Logos SQL or the Zone SDK","understand Cryptarchia, Mantle or the Blend network"],"llms_txt":"https://docs.logos.co/blockchain/llms.txt","llms_full_txt":"https://docs.logos.co/blockchain/llms-full.txt","pages":["/blockchain","/blockchain/get-started/run-blockchain-node-from-basecamp","/blockchain/get-started/run-blockchain-node-from-cli","/blockchain/get-started/claim-leader-rewards-in-basecamp","/blockchain/concepts/about-bedrock","/blockchain/concepts/about-cryptarchia","/blockchain/concepts/about-mantle","/blockchain/concepts/about-the-blend-network","/blockchain/concepts/about-zones","/blockchain/transfer-tokens/move-blockchain-assets-on-basecamp","/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp","/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk","/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone","/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation","/blockchain/blend/join-the-blend-network-as-a-core-node","/blockchain/logos-sql","/blockchain/logos-sql/build-application-with-logos-sql","/blockchain/logos-sql/handle-displaced-writes","/blockchain/faq","/blockchain/reference/blockchain-api-reference"]},{"id":"lez","title":"LEZ (Logos Execution Zone)","description":"The Logos Execution Zone: wallet, accounts, public and private transfers, custom tokens, programs, RLN membership, explorer and chain following.","use_cases":["run the LEZ wallet and transfer native tokens","create and transfer custom tokens on LEZ","write and deploy a LEZ program","register an RLN membership","run a LEZ explorer or follow the chain"],"llms_txt":"https://docs.logos.co/lez/llms.txt","llms_full_txt":"https://docs.logos.co/lez/llms-full.txt","pages":["/lez","/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers","/lez/get-started/run-lez-wallet-via-cli","/lez/transfer-tokens/transfer-native-tokens-on-lez","/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez","/lez/transfer-tokens/transfer-lez-tokens-between-public-private-states","/lez/follow-chain/run-lez-indexer","/lez/follow-chain/start-and-use-instance-of-lez-explorer","/lez/accounts/set-up-shared-private-lez-account","/lez/programs/write-and-deploy-lez-program-with-scaffold","/lez/rln/register-rln-membership-from-basecamp","/lez/rln/register-rln-membership-on-lez-with-delivery-node","/lez/extensions/admin-authority","/lez/extensions/freeze-authority","/lez/extensions/build-a-spel-extension-library"]},{"id":"messaging","title":"Messaging","description":"Logos Messaging: the Delivery node and module API, the Chat module API, Logos Chat 1:1 and group messaging, and peer-discovery concepts.","use_cases":["run a Delivery node","use the Delivery module API from an app","build a module that uses the Chat module API","send 1:1 or group messages with Logos Chat","understand how messaging peers discover each other"],"llms_txt":"https://docs.logos.co/messaging/llms.txt","llms_full_txt":"https://docs.logos.co/messaging/llms-full.txt","pages":["/messaging","/messaging/get-started/send-1-1-messages-logos-chat","/messaging/get-started/send-group-messages-logos-chat","/messaging/get-started/faq","/messaging/concepts/protocols","/messaging/concepts/content-topics","/messaging/concepts/network-domains","/messaging/concepts/transports","/messaging/concepts/understand-peer-discovery/static-peers","/messaging/concepts/understand-peer-discovery/dns-discovery","/messaging/concepts/understand-peer-discovery/discv5","/messaging/concepts/understand-peer-discovery/peer-exchange","/messaging/concepts/security-features","/messaging/concepts/logos-delivery-and-libp2p","/messaging/delivery/use-delivery-module-api-in-app","/messaging/delivery/run-delivery-node-from-cli","/messaging/chat-module/build-logos-module-that-uses-chat-module-api","/messaging/concepts/understand-peer-discovery/peer-discovery"]},{"id":"storage","title":"Storage","description":"Logos Storage: decentralised, content-addressed file storage. Running a storage node and building apps on the Storage API.","use_cases":["run a Logos Storage node from the CLI or Basecamp","upload and download files","build a CLI app that uses the Storage API"],"llms_txt":"https://docs.logos.co/storage/llms.txt","llms_full_txt":"https://docs.logos.co/storage/llms-full.txt","pages":["/storage","/storage/get-started/run-storage-node-from-basecamp","/storage/get-started/run-storage-node-from-cli","/storage/get-started/run-mix-network-of-storage-nodes","/storage/get-started/faq","/storage/concepts/connectivity","/storage/concepts/mix","/storage/build-app/build-cli-app-that-uses-storage-api"]},{"id":"mixnet","title":"Mixnet","description":"The Logos mix network, which hides who talks to whom from network observers.","use_cases":["send anonymised messages over the mix network"],"llms_txt":"https://docs.logos.co/mixnet/llms.txt","llms_full_txt":"https://docs.logos.co/mixnet/llms-full.txt","pages":["/mixnet/get-started/send-anonymised-messages-over-the-mix-network"]},{"id":"peer-discovery","title":"Peer discovery","description":"Peer discovery and connection management without central registries.","use_cases":["build a Logos core module that uses the service discovery API"],"llms_txt":"https://docs.logos.co/peer-discovery/llms.txt","llms_full_txt":"https://docs.logos.co/peer-discovery/llms-full.txt","pages":["/peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api"]},{"id":"run-a-node","title":"Run a node","description":"Path pages for node operators.","use_cases":["run a full node for blockchain, delivery and storage"],"llms_txt":"https://docs.logos.co/run-a-node/llms.txt","llms_full_txt":"https://docs.logos.co/run-a-node/llms-full.txt","pages":["/run-a-node","/run-a-node/expose-node-metrics"]}],"pages":[{"id":"run-a-node/get-started/run-logos-node-blockchain-storage-delivery","title":"Run a Logos node with blockchain, storage, and delivery","summary":"Get started running a full Logos node with all three core modules on testnet v0.3.","section":"run-a-node","doc_type":"procedure","url":"https://docs.logos.co/run-a-node","markdown_url":"https://docs.logos.co/run-a-node.md","permalink":"/run-a-node","source":"https://github.com/logos-co/logos-docs/blob/main/docs/run-a-node/get-started/run-logos-node-blockchain-storage-delivery.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install `logosctl`"},{"depth":2,"text":"Step 2: Prepare the host"},{"depth":2,"text":"Step 3: Install modules"},{"depth":2,"text":"Step 4: Start the Logos node"},{"depth":2,"text":"Step 5: Configure and start the blockchain module"},{"depth":3,"text":"Fund the node by mining"},{"depth":3,"text":"Claim leader rewards"},{"depth":3,"text":"Optional: Join the Blend Network"},{"depth":2,"text":"Step 6: Configure and start the storage module"},{"depth":2,"text":"Step 7: Configure and start the delivery module"},{"depth":2,"text":"Step 8: Verify the full node is healthy"},{"depth":3,"text":"Optional: Run the node unattended with systemd"},{"depth":2,"text":"Appendix: Full blockchain node configuration"}],"markdown":"# Run a Logos node with blockchain, storage, and delivery\n\n#### Get started running a full Logos node with all three core modules on testnet v0.3.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\n> **Note**\n>\n> This document describes how to run Blockchain, Storage, and Delivery nodes together via the CLI. To run these nodes separately (for example, just a Blockchain node or just a Storage node), see their respective tutorial pages:\n>\n> - [Run a Blockchain node from the CLI](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md)\n> - [Run a Delivery node from the CLI](https://docs.logos.co/messaging/delivery/run-delivery-node-from-cli.md)\n> - [Run a Storage node from the CLI](https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md)\n\nThis procedure covers installing and running a single [Logos node](https://docs.logos.co/get-started/glossary.md#logos-node) via one `logosctl` session. `logosctl` starts and controls the node and manages the `blockchain_module`, `storage_module`, and `delivery_module` from within that session. It is intended for node operators who want to join the testnet and contribute to the Logos network. The steps assume a Linux host.\n\n> **Note**\n>\n> Individual module [package](https://docs.logos.co/get-started/glossary.md#package) versions are pinned independently and do not necessarily match the testnet version number.\n\nThe default paths used throughout this procedure are:\n\n```text\n/usr/local/bin/logosctl\n/var/lib/logos-node/.logosctl\n/var/lib/logos-node\n```\n\n> **Info: Prerequisites**\n>\n>\n> - Linux host with a public IPv4 address.\n> - Ports `3000/udp`, `8090/udp`, `8091/tcp`, `9000/udp`, `30303/tcp`, and `30303/udp` open on the host firewall.\n> - Root or `sudo` access to install tools and create system users.\n>\n> Make sure your hardware meets the following requirements for running a blockchain node:\n> - CPU: 2 Cores, 2Ghz. Modern multi-core processor. **Must have ADX instruction support (on x86_64)**, such as Intel Broadwell or later, or any AMD Zen. Generic CPU models such as `kvm64` and `qemu64` hide ADX and cause the blockchain module to crash with `signal 4`.\n>    - Check whether your CPU has ADX support by running:\n>\n>    ```sh\n>    # Linux\n>    grep -o adx /proc/cpuinfo | head -1\n>\n>    # macOS\n>    sysctl -a | grep -i adx\n>    ```\n> - Memory (RAM): Minimal (1 Gb).\n> - Storage:\n>    - For joining the testnet or a short-lived node: SSD with 1-2 GB free.\n>    - Expected for a long-lived mainnet node: SSD with 100+ GB free with ability to expand storage on demand.\n> - Network: Relatively reliable network connection. 1Mbps of free bandwidth.\n>\n> To run a Blend node, make sure you have:\n> - A stable and accessible external IP.\n> - A stable, low-latency connection (10 Mbps+ recommended) to handle multiple concurrent connections (recommended). This is beneficial for effective message blending and random delays.\n\n## What to expect\n\n- You can run a full Logos node with all three modules active and publicly reachable on the testnet.\n- You can verify each [module](https://docs.logos.co/get-started/glossary.md#module) is healthy by querying the daemon and checking live port bindings.\n- You can configure the node for unattended operation using the systemd service pattern described [here](#optional-run-the-node-unattended-with-systemd).\n\n## Step 1: Install `logosctl`\n\nInstall the system dependencies and download the `logosctl` CLI.\n\n1. Install `curl`, `jq`, `tar`, and FUSE support for AppImage binaries:\n\n   ```sh\n   apt-get update\n   apt-get install -y curl jq tar fuse3\n   ```\n\n1. Download the release archive for `logosctl` version 0.3.2. For x86_64 Linux, download:\n\n   ```sh\n   curl -fL \\\n   -o logosctl-x86_64-linux.tar.gz \\\n   https://github.com/logos-co/logos-logoscore-cli/releases/download/0.3.2/logosctl-x86_64-linux.tar.gz\n   ```\n\n   Verify the archive against the SHA-256 digest for the pinned GitHub release asset, then extract it:\n\n   ```sh\n   sha256sum --check <<'EOF'\n   294fbcc6409a8851b7471582bdad2dbfaf681b5d1c67dfffec38ada925dc6762  logosctl-x86_64-linux.tar.gz\n   EOF\n   \n   tar -xzf logosctl-x86_64-linux.tar.gz\n   ```\n\n1. Verify the extracted AppImage:\n\n   ```sh\n   sha256sum --check <<'EOF'\n   dc2a7e67c7326dada2b9302fb15f9df2cacbe6de3b9db50502ad7831608ffd56  logosctl-x86_64.AppImage\n   EOF\n   ```\n\n1. Install the tool under `/usr/local/bin` as `logosctl`:\n\n   ```sh\n   install -m755 logosctl-x86_64.AppImage /usr/local/bin/logosctl\n   ```\n\n1. Verify the tool is accessible:\n\n   ```sh\n   logosctl --version\n   ```\n\n## Step 2: Prepare the host\n\nCreate a new user that will run the Logos node, as well as the `logosctl` session directory and module data directories.\n\n1. Create the `logos` system user and data directories:\n\n   ```sh\n   useradd --system --home /var/lib/logos-node --create-home --shell /usr/sbin/nologin logos\n   mkdir -p /var/lib/logos-node/.logosctl\n   mkdir -p /var/lib/logos-node/blockchain-module-testnet\n   mkdir -p /var/lib/logos-node/storage-module\n   mkdir -p /var/lib/logos-node/delivery-module\n   chown -R logos:logos /var/lib/logos-node\n   chmod 700 /var/lib/logos-node/.logosctl\n   ```\n\n1. Open these on the host firewall:\n\n   ```text\n   3000/udp\n   8090/udp\n   8091/tcp\n   9000/udp\n   30303/tcp\n   30303/udp\n   ```\n\n   If the node is behind NAT, forward these ports to the node using the listed protocols.\n\n## Step 3: Install modules\n\nDownload and install the three module packages from the configured module [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue).\n\n1. As root, open a shell as the `logos` user. Setting `HOME` selects the default `/var/lib/logos-node/.logosctl` session:\n\n   ```sh\n   runuser -u logos -- env HOME=/var/lib/logos-node bash\n   ```\n\n1. Initialise the session with the default daemon configuration:\n\n   ```sh\n   printf '{}\\n' | logosctl daemon config set -\n   ```\n\n1. Temporarily start the Logos node in detached mode so its bundled package-management modules are available:\n\n   ```sh\n   logosctl daemon start --detach\n   logosctl daemon status\n   ```\n\n   - The detached command returns after the Logos node is ready to accept commands.\n\n1. Refresh the official module catalogue. Run this even if the daemon was already running, because it does not see packages published after it last refreshed:\n\n   ```sh\n   logosctl catalog refresh\n   ```\n\n1. Install the pinned module packages. The root hashes ensure you select the published package identity that exactly matches the pinned version:\n\n   ```sh\n   logosctl package install blockchain_module \\\n   --version 0.3.4 \\\n   --root-hash 911a849083727457c25df4c29b2f2efd7c5b2906cd8f45eaa7f0cd89178aa08e \\\n   --yes\n   logosctl package install storage_module \\\n   --version 3.0.2 \\\n   --root-hash 810c39c610c0f37cc6e48e5fb115df69f72de4df7c052cdb1e17f53b7897cc97 \\\n   --yes\n   logosctl package install delivery_module \\\n   --version 0.3.2 \\\n   --root-hash 824ccb2cd21b54f196dd3d32659df7f67e6df4298b1ab8cab60b48f704e2feac \\\n   --yes\n   ```\n\n   > **Note**\n   >\n   > Installing a package does not load it into the running Logos node. Packages must be loaded separately.\n\n1. Check the installed core packages:\n\n   ```sh\n   logosctl package ls --type core\n   ```\n\n   - The output must list:\n\n   ```text\n   blockchain_module 0.3.4\n   delivery_module 0.3.2\n   storage_module 3.0.2\n   ```\n\n1. Stop the Logos node after installation:\n\n   ```sh\n   logosctl daemon stop\n   ```\n\n   - In the next section, you will restart the node for normal operation.\n\n## Step 4: Start the Logos node\n\nStart the `logosctl` daemon before loading any modules. Make sure to run the node controls, module configuration, module calls, and health checks from the `logos` user shell created in the previous section. This keeps the Logos node and `logosctl` client on the same `/var/lib/logos-node/.logosctl` session and ensures generated files belong to `logos`.\n\n> **Tip**\n>\n> If the window was closed, reopen it with:\n>\n> ```sh\n> runuser -u logos -- env HOME=/var/lib/logos-node bash\n> ```\n\n1. Set the Logos node working directory:\n\n   ```sh\n   cd /var/lib/logos-node\n   ```\n\n1. For a manual foreground run, start the Logos node with:\n\n   ```sh\n   logosctl daemon start\n   ```\n\n   - Keep this terminal open. Use another `logos` user shell for all module commands.\n\n   > **Tip**\n   >\n   > For unattended operation, use a [systemd service](#optional-run-the-node-unattended-with-systemd) rather than a manually started daemon.\n\n1. Verify the daemon is running:\n\n   ```sh\n   logosctl daemon status\n   ```\n\n## Step 5: Configure and start the blockchain module\n\nLoad the blockchain module, generate the node config, and start the module.\n\n> **Warning**\n>\n> The blockchain module `0.3.1` release started a new blockchain with a new genesis, and `0.3.2` and `0.3.4` continue on it.\n>\n> If your node last ran a release earlier than `0.3.1`, start it with an empty blockchain state directory. Balances and Blend declarations from the previous blockchain do not carry over. A node upgrading from `0.3.1` or `0.3.2` keeps its state.\n\n> **Info: Upgrading from `0.3.2`**\n>\n> Keep your existing `user_config.yaml` and `keystore.yaml`. On its first start, the `0.3.4` module rewrites a `0.3.2` `user_config.yaml` in the new format, keeps a copy of the original next to it as `user_config.yaml.0.3.2.bak`, and removes settings that only repeat the defaults. To upgrade, install the `0.3.4` package as in [Step 3](#step-3-install-modules), restart the daemon, load the module, and run the `start` command below. Do not run `generate_user_config` again.\n\n1. Create the peer bootstrap file:\n\n   ```sh\n   cd /var/lib/logos-node/blockchain-module-testnet\n   cat > peers.json <<EOF\n   {\n     \"initial_peers\": [\n       \"/ip4/65.109.51.37/udp/3000/quic-v1/p2p/12D3KooWFrouXfmrR4nsLMtE7wu15DoMJ6VtoUtHinREZCvbWHar\",\n       \"/ip4/65.109.51.37/udp/3001/quic-v1/p2p/12D3KooWJRGau8M1rjT7R5e4YYsgdFhsMX35nRDtMwCDjxQkXAHz\",\n       \"/ip4/65.109.51.37/udp/3002/quic-v1/p2p/12D3KooWQXJavMDTRscjauFSgVAB1VLB6Rzpy2uY5SU9Tk7927tb\",\n       \"/ip4/65.109.51.37/udp/50001/quic-v1/p2p/12D3KooWSQc7CcGtvWDPF1yCbBthFnQjprfCVHmfmNDUrSmqQsU1\"\n     ]\n   }\n   EOF\n   ```\n\n1. Load the module and generate `user_config.yaml`:\n\n   ```sh\n   logosctl module load blockchain_module\n   cd /var/lib/logos-node/blockchain-module-testnet\n   logosctl call blockchain_module generate_user_config @peers.json\n   chmod 600 /var/lib/logos-node/user_config.yaml /var/lib/logos-node/keystore.yaml\n   ```\n\n   > **Info**\n   >\n   > `keystore.yaml` holds the node's secret keys, and the node reads it together with `user_config.yaml`. Keep both files private, restrict file permissions, and do not publish them. Generate fresh files for each node.\n\n   - `generate_user_config` writes `user_config.yaml` and `keystore.yaml` to the Logos node's working directory (`/var/lib/logos-node/` with this guide's layout).\n   - The generated `user_config.yaml` lists only the bootstrap peers. Every other setting uses its default value, so it does not appear in the file. To change a setting, add it to `user_config.yaml` under its full path and restart the blockchain module. For example, to change the P2P port:\n\n   ```yaml\n   network:\n     backend:\n       swarm:\n         port: 3000\n   ```\n\n   - Settings you may want to change are listed below. For every setting and its default, see [Appendix: Full blockchain node configuration](#appendix-full-blockchain-node-configuration).\n\n   | Field | Purpose | Default and guidance |\n   |-------|---------|----------|\n   | `network.backend.initial_peers` | Bootstrap peers | Generated from `peers.json`. Use the current network document |\n   | `network.backend.swarm.port` | Public UDP P2P port | `3000`. Keep aligned with firewall/NAT |\n   | `api.backend.listen_address` | Local API bind | `127.0.0.1:8080`. Keep private |\n   | `state.base_folder` | State directory | `./state`, relative to the Logos node's working directory (`/var/lib/logos-node/state` with this guide's layout). Use a persistent local path |\n   | `tracing.level` | Log verbosity | `INFO`. Use `INFO` for unattended operation |\n   | `tracing.metrics.open_metrics.enabled` | Node metrics | `true`. See [expose the node's metrics](https://docs.logos.co/run-a-node/expose-node-metrics.md) |\n   | `blend.core.backend.listening_address` | Blend listener | `/ip4/0.0.0.0/udp/3400/quic-v1`. Only used once the node [joins the Blend Network](#optional-join-the-blend-network) |\n\n   - The node writes hourly log files named `logos-blockchain.log.<date-hour>` to its working directory. `logos-blockchain.log.latest` links to the current one. A node upgraded from `0.3.2` keeps the log file prefix it used before, which `tracing.logger.file.prefix` in `user_config.yaml` shows.\n\n1. Start the blockchain module:\n\n   ```sh\n   logosctl call blockchain_module start /var/lib/logos-node/user_config.yaml \"\"\n   ```\n\n   - The second argument is intentionally an empty string; the blockchain module no longer requires a downloaded `deployment.yaml` file.\n\n1. Verify the module is running:\n\n   ```sh\n   logosctl call blockchain_module get_cryptarchia_info | jq -r .result.value | jq .\n   ```\n\n   - Your node will take about an hour to finish [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping) and enter the `Online` state.\n   - If no bootstrap peer answers when the node starts, the node stops itself, and `result.error` reads `The node is no longer running, most likely because one of its services panicked`. The node log shows `Initial Block Download failed: AllPeersFailed`. Check that outbound UDP traffic is allowed and that `peers.json` lists the current bootstrap peers, then restart the module:\n\n   ```sh\n   logosctl call blockchain_module stop\n   logosctl call blockchain_module start /var/lib/logos-node/user_config.yaml \"\"\n   ```\n\n   - After a restart, the node first replays the blocks it already stored. The `start` call, and every `logosctl call blockchain_module` command until the replay ends, can return `RPC call failed`. The node still starts. The replay takes longer the more blocks the node has stored, from seconds on a new node to 10 minutes or more on a node that has synced the testnet. The node log reports `found <N> stored blocks to replay during chain recovery` when it begins.\n\n   > **Warning**\n   >\n   > Do not call `pow_status` or `pow_claimable_rewards` before the node is `Online`. While the node is bootstrapping these calls never return, and every later `logosctl call blockchain_module` command fails with `RPC call failed` until you restart the daemon with `logosctl daemon stop`.\n\n1. To participate in consensus, your node needs funds. Fund it by mining, as described in the next section.\n\n### Fund the node by mining\n\nBy default, the node automatically claims its mining rewards to its `PoWClaim` key. Find this public key at `public_keys.PoWClaim` in `/var/lib/logos-node/keystore.yaml`.\n\n1. The `pow` settings do not appear in the generated `user_config.yaml`. Without a `pow` section, the node uses these defaults, where `<your PoWClaim key>` is the key from `keystore.yaml`:\n\n   ```yaml\n   pow:\n     mining:\n       max_threads: 1\n       max_tickets_per_block: 1\n     auto_claim:\n       targets:\n       - public_key: <your PoWClaim key>\n         threshold: 2000000000\n       tick:\n         unit: seconds\n         value: 10\n   ```\n\n   - `max_threads` sets how many CPU threads mine. To mine faster, add `pow.mining.max_threads` to `user_config.yaml` with a higher value, or set it to `null` to use one thread per CPU core.\n   - `threshold` is the balance, in tokens, that auto-claim pays the target up to. Once a target reaches its threshold, auto-claim stops paying it, and once every target has, auto-claim and mining both stop.\n   - If you add `pow.auto_claim.targets` to `user_config.yaml`, it replaces the default list, so include your `PoWClaim` key in it.\n   - If you edit the `pow` section, restart the blockchain module for the change to take effect.\n\n1. After your node reaches `Online` mode, start mining:\n\n   ```sh\n   logosctl call blockchain_module pow_start_mining\n   ```\n\n   - Mining is turned off by default and does not persist across restarts. Run `pow_start_mining` again after every restart.\n   - Auto-claim starts automatically. You do not need to call `pow_start_auto_claim`.\n\n1. Check the mining and auto-claim status:\n\n   ```sh\n   logosctl call blockchain_module pow_status | jq -r .result.value | jq .\n   ```\n\n   - `is_mining` is `true`, and the `balance` of the auto-claim target grows as auto-claim collects rewards.\n\n### Claim leader rewards\n\nOnce your mined funds have aged into the leadership lottery, which can take up to two [epochs](https://docs.logos.co/get-started/glossary.md#epoch), your node takes part in consensus automatically. Each block it proposes mints a leader-reward voucher. Vouchers are not credited automatically; claim them to turn them into spendable balance.\n\nRun these commands only while the node is `Online`. The chain-leader service is not available while the node is still `Bootstrapping`.\n\n1. Check which vouchers are claimable:\n\n   ```sh\n   logosctl call blockchain_module wallet_get_claimable_vouchers | jq -r .result.value | jq .\n   ```\n\n   - `vouchers` lists the vouchers your wallet can claim now. It is empty until your node has proposed a block, and again once every voucher has been claimed.\n   - `tip` is the block the vouchers were checked at.\n\n1. Claim a voucher:\n\n   ```sh\n   logosctl call blockchain_module leader_claim\n   ```\n\n   - On success, the call returns `success: true` with the claim transaction hash in `value`.\n   - Each call claims one voucher. To claim several, call `leader_claim` once per listed voucher, pausing a few seconds between calls.\n   - If there is nothing to claim, `result.error` reads `No claimable voucher found`. A voucher becomes claimable only once it is provable against the current tip, so this can happen shortly after your node proposes a block. Retry later.\n   - If the reply has `\"status\": \"error\"` and no `result` object, the call never reached the chain-leader service, either because it timed out or because the service is not running yet. Confirm the node is `Online` with `get_cryptarchia_info` rather than retrying the claim.\n\n### Optional: Join the Blend Network\n\nWith a running [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) node, it is possible - but not necessary - to participate in the [Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network).\n\n1. Fund both the `BlendZk` and `SdpFunding` keys from your `keystore.yaml` by mining into your `PoWClaim` key, then transferring funds from it to each key, as described in [Join the Blend Network as a core node](https://docs.logos.co/blockchain/blend/join-the-blend-network-as-a-core-node.md).\n\n   > **Info**\n   >\n   > The public keys and [note](https://docs.logos.co/get-started/glossary.md#note) IDs below are examples. Use the corresponding values from your own `keystore.yaml` and wallet responses when running these commands.\n\n   ```bash\n   # keystore.yaml\n   public_keys:\n      ...\n   BlendZk: 13cccf99f90fd78c2134891ce3c1afce0605753a7694b9d56678d63a8d471820\n      ...\n   SdpFunding: 91d381a87e05d46fc9bc95246273b6930290506f0589ad039444decd3c24940e\n      ...\n   ```\n\n1. Wait until both keys have received funds. Check each balance with `wallet_get_notes`:\n\n   ```bash\n   logosctl call blockchain_module wallet_get_notes <ADDRESS> \"\" | jq -r .result.value | jq .notes\n   ```\n\n1. Join the Blend Network by locking one of the notes held by your `BlendZk` key.\n\n   > **Info**\n   >\n   > Make sure to open `<YOUR_BLEND_PORT>/udp` on the public host firewall before running the following command. `<YOUR_BLEND_PORT>` is `3400` unless you set `blend.core.backend.listening_address` in `user_config.yaml`. Configure the firewall and NAT forwarding before joining and verify the local listener and public reachability after activation.\n\n   ```sh\n   logosctl call blockchain_module blend_join_as_core_node \\\n      \"/ip4/<YOUR_IP>/udp/<YOUR_BLEND_PORT>/quic-v1\" \\\n      \"<BLEND_ZK_NOTE_ID>\"\n   ```\n\n   - `<YOUR_IP>`: Must be your external IP address\n   - `<YOUR_BLEND_PORT>`: Your Blend port, `3400` by default, or the port in `blend.core.backend.listening_address` if you set it in `user_config.yaml`. Note that if you do port-mapping, the external mapped port must be used.\n   - `<BLEND_ZK_NOTE_ID>`: The note ID of one of the notes held by your `BlendZk` key, as queried above.\n   - The Blend core listener starts only after the node's declaration becomes active.\n\n1. Verify the declaration was accepted on chain by polling `/mantle/sdp/declarations`, looking for your declaration\n\n   ```\n   curl http://127.0.0.1:8080/mantle/sdp/declarations | jq . \n   # > {\n   # >   \"<DECLARATION_ID>\": {\n   # >     \"service_type\": \"BN\",\n   # >     \"provider_id\": \"35d60d973560b8344f83dc266a3fe89e35a3dcf9959c492d0a7a0b7a85c5d2ce\",\n   # >     \"locked_note_id\": \"<BLEND_ZK_NOTE_ID>\",\n   # >     \"locators\": [\n   # >       \"/ip4/<YOUR_IP>/udp/<YOUR_BLEND_PORT>/quic-v1\"\n   # >     ],\n   # >     \"zk_id\": \"13cccf99f90fd78c2134891ce3c1afce0605753a7694b9d56678d63a8d471820\",\n   # >     \"created\": 1,\n   # >     \"active\": 3,\n   # >     \"withdraw_at\": null,\n   # >     \"nonce\": 0\n   # >   }\n   # > }\n   ```\n\n   - The response is a JSON object keyed by declaration id (not a list). Find your entry by its `provider_id` (your BlendSigning key) or `zk_id` (your BlendZk key).\n   - `service_type: BN` identifies it as a [Blend node](https://docs.logos.co/get-started/glossary.md#blend-node) declaration.\n   - `created` is the epoch your declaration was included; it takes effect about two [epochs](https://docs.logos.co/get-started/glossary.md#epoch) later. `active` is the most recent epoch your node has re-attested activity for (via the periodic Active message), so it advances over time—equal to `created + 2` right after activation and higher on a long-running node.\n\n## Step 6: Configure and start the storage module\n\nIn `logosctl` 0.3.2, the package downloader starts Storage automatically using its saved configuration, or defaults on first use.\n\n1. Check that the [Storage module](https://docs.logos.co/get-started/glossary.md#storage-module) is running:\n\n   ```bash\n   logosctl call storage_module isRunning\n   ```\n\n   Repeat this check until `result` is `true` before downloading. Startup can take a few minutes.\n\n1. Try downloading the book [Farewell to Westphalia](https://logos.co/book):\n\n   ```sh\n   logosctl call storage_module downloadToUrl zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\\\n      \"$(pwd)/farewell-to-westphalia.pdf\" false 65536 false false\n   ```\n   \n   After a while - a few seconds, depending on your internet connection - the file should appear on your disk.\n\n1. Logos Storage supports private downloads over the [Logos mix network](https://docs.logos.co/storage/concepts/mix.md). They are slow, but prevent others from learning that you are downloading a file. Try it out:\n\n   ```sh\n   # remove file from disk\n   rm ./farewell-to-westphalia.pdf\n   # remove file from node\n   logosctl call storage_module remove zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\n   # download again, this time using mix\n   logosctl call storage_module downloadToUrl zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\\\n      \"$(pwd)/farewell-to-westphalia.pdf\" false 65536 true false\n   ```\n\n   In contrast to direct downloads, downloads over mix can take a few minutes. You should see the file streaming to your disk, though, and eventually the download should complete. \n\n   > **Tip**\n   >\n   > - Use **absolute paths** when feeding file paths to Logos Storage via the module API. Relative paths resolve relative to the daemon's working directory, which might be different from what you expect.\n   > - On a freshly started node, the first calls can return `\"error\":\"Failed to start download.\"` while the mix relays connect. Wait about 10 seconds and run `downloadToUrl` again.\n\n## Step 7: Configure and start the delivery module\n\nCreate the kernel-only delivery config for a node operator and start the module. Replace `<public-ip>` with the node's public IPv4 address before running these commands.\n\n1. Create the delivery config:\n\n   ```sh\n   cd /var/lib/logos-node/delivery-module\n   cat > config.json <<EOF\n   {\n      \"entryLayer\": \"kernel\",\n      \"kernelConf\": {\n         \"preset\": \"logos.test\",\n         \"relay\": true,\n         \"logLevel\": \"INFO\",\n         \"tcpPort\": 30303,\n         \"discv5UdpPort\": 9000,\n         \"discv5Discovery\": true,\n         \"nat\": \"extip:<public-ip>\"\n      }\n   }\n   EOF\n   ```\n\n   - `config.json` includes the following fields:\n\n   | Field | Purpose |\n   |-------|---------|\n   | `entryLayer` | Delivery stack layer; use `kernel` for a node-operator service |\n   | `kernelConf` | Kernel node configuration |\n   | `kernelConf.preset` | Network preset |\n   | `kernelConf.relay` | Enable the [Relay](https://docs.logos.co/get-started/glossary.md#relay) protocol |\n   | `kernelConf.logLevel` | Log verbosity |\n   | `kernelConf.tcpPort` | Public TCP P2P port; QUIC uses the same port number over UDP by default |\n   | `kernelConf.discv5UdpPort` | Public UDP discovery port |\n   | `kernelConf.discv5Discovery` | Enable discv5 discovery |\n   | `kernelConf.nat` | Public IP advertisement mode |\n\n   - The kernel-only entry layer intentionally omits the messaging client and reliable channel manager.\n   - Calls to `send`, `subscribe`, and `channel*` are unavailable, while `getNodeInfo`, `storeQuery`, and metrics remain available.\n   - Use fixed `tcpPort` and `discv5UdpPort`; do not leave public nodes on random ports.\n   - Delivery `0.3.0` enables QUIC by default. Open and, if needed, forward both TCP and UDP on `tcpPort` (`30303` here), plus UDP on `discv5UdpPort` (`9000`).\n   - The `logos.test` preset provides the delivery network bootstrap settings.\n\n1. Load and start the [delivery module](https://docs.logos.co/get-started/glossary.md#delivery-module):\n\n   ```sh\n   cd /var/lib/logos-node/delivery-module\n   logosctl module load delivery_module\n   logosctl call delivery_module createNode @config.json\n   logosctl call delivery_module start\n   ```\n\n1. Verify the delivery module is running:\n\n   ```sh\n   logosctl call delivery_module getAvailableNodeInfoIDs\n   logosctl call delivery_module getNodeInfo Version\n   logosctl call delivery_module getNodeInfo MyMultiaddresses\n   ```\n\n## Step 8: Verify the full node is healthy\n\nRun health checks against the Logos node and all three loaded modules to confirm the node is fully operational.\n\n1. Check the daemon and all loaded modules:\n\n   ```sh\n   logosctl daemon status --json | jq .\n   logosctl module ls --loaded\n   ```\n\n   Expected modules in the output: `blockchain_module`, `capability_module`, `delivery_module`, `liblogos_lez_rln_module`, `liblogos_rln_module`, `libp2p_module`, `modules_state`, `package_downloader`, `package_manager`, `storage_module`.\n\n1. Verify all ports are bound correctly:\n\n   ```sh\n   ss -lntup | egrep '(:3000|:8090|:8091|:9000|:30303|:8080)'\n   ```\n\n   Expected bindings:\n\n   ```text\n   0.0.0.0:3000/udp\n   0.0.0.0:8090/udp\n   0.0.0.0:8091/tcp\n   0.0.0.0:9000/udp\n   0.0.0.0:30303/tcp\n   0.0.0.0:30303/udp\n   127.0.0.1:8080/tcp\n   ```\n\n1. Check the blockchain module sync state:\n\n   ```sh\n   logosctl call blockchain_module get_cryptarchia_info | jq -r .result.value | jq .\n   ```\n\n1. (Optional) Check the configured Blend UDP listener:\n\n   ```sh\n   ss -lun\n   ```\n\n   - Confirm that the local Blend UDP port (`3400` unless you set `blend.core.backend.listening_address`) is present. If the public Blend port differs, also confirm that NAT forwards `<YOUR_BLEND_PORT>/udp` to this local port.\n\n1. Check the delivery module bound ports:\n\n   ```sh\n   logosctl call delivery_module getNodeInfo MyBoundPorts\n   ```\n\n1. (Optional) To monitor the node continuously, [expose its metrics to Prometheus](https://docs.logos.co/run-a-node/expose-node-metrics.md).\n\n### Optional: Run the node unattended with systemd\n\nUse a dedicated service for the Logos node process (started and controlled by `logosctl`) and a separate bootstrap script for module startup. Do not start modules from `ExecStartPost` in `logos-node.service`—slow or failing module starts may cause systemd to kill the daemon.\n\nCreate `/etc/systemd/system/logos-node.service`:\n\n```ini\n[Unit]\nDescription=Logos node managed by logosctl\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nUser=logos\nGroup=logos\nWorkingDirectory=/var/lib/logos-node\nEnvironment=HOME=/var/lib/logos-node\nEnvironment=LOGOSCTL_CONFIG_DIR=/var/lib/logos-node/.logosctl\nExecStart=/usr/local/bin/logosctl daemon start\nExecStop=/usr/local/bin/logosctl daemon stop\nRestart=always\nRestartSec=10\nStandardOutput=journal\nStandardError=journal\n[Install]\nWantedBy=multi-user.target\n```\n\nThe separate bootstrap script should wait for `logosctl daemon status`, load and start the blockchain module, load and start the storage module, and load and start the delivery module. It should tolerate already-loaded modules and slow module starts.\n\nRecommended journald retention to cap disk usage:\n\n```ini\n[Journal]\nSystemMaxUse=200M\nSystemKeepFree=1G\nMaxRetentionSec=7day\nMaxFileSec=1day\n```\n\nUse the `INFO` log level for unattended operation; use `DEBUG` only for short troubleshooting windows.\n\n## Appendix: Full blockchain node configuration\n\nThe Logos Docs repository includes a complete, annotated [sample `user_config.yaml` file](https://github.com/logos-co/logos-docs/tree/main/resources/configs/sample_blockchain_user_config.yaml) for `blockchain_module` version `0.3.4`. It holds every setting with its default value, and the values the node otherwise reads from `keystore.yaml`, as they would be for the `user_config.yaml` and `keystore.yaml` that `generate_user_config` writes. Use it to look up the full path, meaning, and default value of a setting.\n\n> **Danger**\n>\n> The keys below are sample values from a throwaway keystore. Never use them for a node: anyone can read them here and spend funds held by these keys. Your own keys are in your `keystore.yaml`.\n\n- Keep your `user_config.yaml` short. Add only the settings you change, under their full path, and let the node fill in the rest.\n- Do not copy the `node_key`, `kms`, `wallet`, key ID, or `funding_pk` values into `user_config.yaml`. The node reads them from `keystore.yaml`, which `kms.keystore` points to.\n- To print the configuration your own node runs with, run the standalone node binary from the [`0.3.4` release](https://github.com/logos-blockchain/logos-blockchain/releases/tag/0.3.4). It loads the configuration the same way the module does, and prints secret keys as `<secret>`:\n\n  ```sh\n  logos-blockchain-node /var/lib/logos-node/user_config.yaml --show-config\n  ```\n"},{"id":"run-a-node/get-started/expose-logos-node-metrics-to-prometheus","title":"Expose Logos node metrics to Prometheus","summary":"Serve the blockchain, storage, and delivery metrics of a running Logos node on one OpenMetrics endpoint and scrape it with Prometheus.","section":"run-a-node","doc_type":"procedure","url":"https://docs.logos.co/run-a-node/expose-node-metrics","markdown_url":"https://docs.logos.co/run-a-node/expose-node-metrics.md","permalink":"/run-a-node/expose-node-metrics","source":"https://github.com/logos-co/logos-docs/blob/main/docs/run-a-node/get-started/expose-logos-node-metrics-to-prometheus.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install the `openmetrics` module"},{"depth":2,"text":"Step 2: Check the blockchain node's metrics"},{"depth":2,"text":"Step 3: Start the metrics endpoint"},{"depth":2,"text":"Step 4: Check the endpoint"},{"depth":2,"text":"Step 5: Keep the endpoint private"},{"depth":2,"text":"Step 6: Scrape the node with Prometheus"},{"depth":2,"text":"Step 7: Start the endpoint with the node"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"`logosctl call openmetrics start` returns `\"result\":0`"},{"depth":3,"text":"A module's metrics are missing from `/metrics`"}],"markdown":"# Expose Logos node metrics to Prometheus\n\n#### Serve the blockchain, storage, and delivery metrics of a running Logos node on one OpenMetrics endpoint and scrape it with Prometheus.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure covers adding the `openmetrics` [module](https://docs.logos.co/get-started/glossary.md#module) to a [Logos node](https://docs.logos.co/get-started/glossary.md#logos-node) and pointing [Prometheus](https://prometheus.io/) at it. `openmetrics` runs a small HTTP server inside the node. On every scrape, it asks each module you list for its metrics and returns them as one [OpenMetrics](https://prometheus.io/docs/specs/om/open_metrics_spec/) document, with a `module` label on every series. It is intended for node operators who want dashboards and alerts for a node they already run.\n\nThe steps continue from [Run a Logos node with blockchain, storage, and delivery](https://docs.logos.co/run-a-node.md) and use the same `logos` user, paths, and `logosctl` session.\n\n> **Info: Prerequisites**\n>\n>\n> - A Logos node set up with [Run a Logos node with blockchain, storage, and delivery](https://docs.logos.co/run-a-node.md), with the daemon running and the blockchain, storage, and delivery modules started.\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n> - A [Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) server that can reach the node over TCP, on the same host or on your private network.\n\n## What to expect\n\n- You can scrape blockchain, storage, and delivery metrics from a single `/metrics` endpoint on the node.\n- You can see the node as a healthy target in Prometheus and query its series by `module` label.\n- You can keep the endpoint private and bring it back automatically after the node restarts.\n\n## Step 1: Install the `openmetrics` module\n\nInstall the module into the running node. As root, open a shell as the `logos` user, and run every `logosctl` command in this procedure from it:\n\n```sh\nrunuser -u logos -- env HOME=/var/lib/logos-node bash\n```\n\n1. Install the pinned `openmetrics` [package](https://docs.logos.co/get-started/glossary.md#package) from the module [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue):\n\n   ```sh\n   logosctl package install openmetrics \\\n   --version 0.1.2 \\\n   --root-hash 15b22b2020bf83f775b6d55f2e62ceb85582d1720c5d56099b8edd33928a13bb \\\n   --yes\n   ```\n\n   - The running node picks up the new package without a restart.\n\n1. Load the module:\n\n   ```sh\n   logosctl module load openmetrics\n   ```\n\n   - The output must include `\"module\":\"openmetrics\",\"status\":\"ok\"`.\n\n## Step 2: Check the blockchain node's metrics\n\nSince blockchain module `0.3.4`, the node reports its metrics by default. The setting is `tracing.metrics.open_metrics.enabled`, and it is `true` unless `user_config.yaml` sets it to `false`. A `0.3.2` configuration that the node upgrades on its first `0.3.4` start also has metrics on.\n\n1. Check that the node reports metrics:\n\n   ```sh\n   logosctl call blockchain_module collectOpenMetricsText\n   ```\n\n   - The result is an OpenMetrics document with `# TYPE` lines. If it is, continue with [Step 3](#step-3-start-the-metrics-endpoint).\n   - An empty result means the metrics are off or the node is not running. Check the node with `logosctl call blockchain_module get_cryptarchia_info`, and look for the setting in the configuration:\n\n   ```sh\n   grep -n -A1 open_metrics /var/lib/logos-node/user_config.yaml\n   ```\n\n1. If the configuration has `enabled: false` under `tracing.metrics.open_metrics`, remove those lines or change the value to `true`, then restart the blockchain module so it reads the new setting:\n\n   ```sh\n   logosctl module unload blockchain_module\n   logosctl module load blockchain_module\n   logosctl call blockchain_module start /var/lib/logos-node/user_config.yaml \"\"\n   ```\n\n   - The node keeps its state and resumes from the blocks it already stored.\n   - The `start` call can return `RPC call failed`, or `call to 'blockchain_module.start' timed out after 20000ms`, while the node replays its stored blocks. The node still starts. Wait a few minutes, then check `logosctl call blockchain_module get_cryptarchia_info`.\n\n1. Run `collectOpenMetricsText` again. The result is now an OpenMetrics document with `# TYPE` lines.\n\n## Step 3: Start the metrics endpoint\n\nTell `openmetrics` which port to listen on and which modules to collect from.\n\n1. Create the endpoint configuration:\n\n   ```sh\n   mkdir -p /var/lib/logos-node/openmetrics\n   cd /var/lib/logos-node/openmetrics\n   cat > config.json <<'EOF'\n   {\n     \"port\": 9464,\n     \"modules\": [\n       { \"name\": \"blockchain_module\", \"format\": \"text\" },\n       \"storage_module\",\n       { \"name\": \"delivery_module\", \"format\": \"text\" }\n     ]\n   }\n   EOF\n   ```\n\n   | Field | Purpose |\n   |-------|---------|\n   | `port` | TCP port of the HTTP server. Pick a free port; `9090` is the default port of Prometheus itself |\n   | `modules` | Modules to collect from on each scrape |\n   | `modules[].name` | Module name, as shown by `logosctl module ls --loaded` |\n   | `modules[].format` | `data` (the default, also used for a bare name) or `text` for modules that already produce OpenMetrics text |\n\n   - `storage_module` returns structured metrics, so a bare name is enough.\n   - `blockchain_module` and `delivery_module` return OpenMetrics text and need `\"format\": \"text\"`. Listed as a bare name, they contribute no metrics.\n\n1. Start the endpoint:\n\n   ```sh\n   logosctl call openmetrics start @config.json\n   ```\n\n   - `\"result\":1` means the server is listening. `\"result\":0` means it did not start; see [Troubleshooting](#troubleshooting).\n\n1. Confirm it is serving:\n\n   ```sh\n   logosctl call openmetrics getInfo\n   ```\n\n   - The result must contain `\\\"running\\\":true` and `\\\"port\\\":9464`.\n\n## Step 4: Check the endpoint\n\n1. Check that the server answers:\n\n   ```sh\n   curl http://127.0.0.1:9464/health\n   ```\n\n   - The response is `ok`.\n\n1. Fetch the metrics:\n\n   ```sh\n   curl -s http://127.0.0.1:9464/metrics | grep -E 'consensus_tip_height|consensus_peers_connected|libp2p_peers|kad_routing_table_peers|repostore_bytes_used'\n   ```\n\n   - **Expected result:** gauges from all three modules, each tagged with the module that reported it. Your values differ:\n\n   ```text\n   # TYPE consensus_peers_connected gauge\n   consensus_peers_connected{module=\"blockchain_module\",otel_scope_name=\"logos-blockchain-node\"} 127\n   # TYPE consensus_tip_height gauge\n   consensus_tip_height{module=\"blockchain_module\",otel_scope_name=\"logos-blockchain-node\"} 3481\n   # HELP libp2p_peers total connected peers\n   # TYPE libp2p_peers gauge\n   libp2p_peers{module=\"delivery_module\"} 15.0\n   # HELP logos_storage_kad_routing_table_peers total peers in routing table\n   # TYPE logos_storage_kad_routing_table_peers gauge\n   logos_storage_kad_routing_table_peers{module=\"storage_module\"} 21.0\n   # HELP logos_storage_repostore_bytes_used storage repostore bytes used\n   # TYPE logos_storage_repostore_bytes_used gauge\n   logos_storage_repostore_bytes_used{module=\"storage_module\"} 31522926.0\n   ```\n\n   - The full document holds a few hundred metric families and ends with `# EOF`. Storage metrics start with `logos_storage_` and delivery metrics with `logos_delivery_`. The [delivery module](https://docs.logos.co/get-started/glossary.md#delivery-module) also reports its libp2p, discovery, and mix network metrics.\n   - Blockchain metrics have no common prefix (`consensus_`, `chainsync_`, `mempool_`, `network_`, `storage_`, and others) and no `# HELP` lines. Filter them by `module=\"blockchain_module\"`.\n   - A blockchain series appears once the node has recorded it, so a node that just started reports fewer families.\n   - A module that is not running, or that fails to answer, is left out of the scrape. The other modules are still reported.\n\n## Step 5: Keep the endpoint private\n\nThe metrics server listens on all network interfaces, not only on `127.0.0.1`. The metrics reveal peer counts, traffic, and storage use, so expose the port only to your Prometheus server.\n\n1. Confirm the listening address:\n\n   ```sh\n   ss -lntp | grep ':9464'\n   ```\n\n   - The output shows `0.0.0.0:9464`.\n\n1. Make sure the host firewall does not open port `9464/tcp` to the internet. If Prometheus runs on another host, allow only that host. For example, with `ufw`, replace `<prometheus-ip>` with the address of your Prometheus server:\n\n   ```sh\n   sudo ufw allow from <prometheus-ip> to any port 9464 proto tcp\n   ```\n\n   - Run this from an account with `sudo` access, not from the `logos` user shell.\n\n## Step 6: Scrape the node with Prometheus\n\n1. Add a scrape job for the node to your Prometheus configuration (`prometheus.yml`). Add it as a new item under the existing `scrape_configs:` key. A second `scrape_configs:` key makes Prometheus reject the file. Replace `<node-address>` with `127.0.0.1` if Prometheus runs on the node, or with the node's private address otherwise:\n\n   \n\n1. Reload or restart Prometheus so it reads the new job.\n\n1. Check that Prometheus scrapes the node. In the Prometheus UI, open **Status > Target health**, or query the API from the Prometheus host (default port `9090`):\n\n   ```sh\n   curl -s --get http://127.0.0.1:9090/api/v1/query --data-urlencode 'query=up{job=\"logos-node\"}'\n   ```\n\n   - The `logos-node` target is up, and the query returns the value `\"1\"`.\n\n1. Query the node's series, for example the number of series each module reports:\n\n   ```text\n   count by (module) ({job=\"logos-node\"})\n   ```\n\n   - The result lists `blockchain_module`, `storage_module`, and `delivery_module`. A few series without a `module` label, such as `up`, are added by Prometheus itself.\n\n## Step 7: Start the endpoint with the node\n\nThe endpoint does not persist across restarts: when the node daemon restarts, the `openmetrics` module is unloaded and the server stops.\n\n1. If you run the node with the [systemd service](https://docs.logos.co/run-a-node.md#optional-run-the-node-unattended-with-systemd), add these commands to the module bootstrap script, after the blockchain, storage, and delivery modules start:\n\n   ```sh\n   logosctl module load openmetrics\n   logosctl call openmetrics start @/var/lib/logos-node/openmetrics/config.json\n   ```\n\n   - Collection only reports modules that are running, so start `openmetrics` after the modules it scrapes.\n   - The blockchain module reports metrics after every restart, unless `user_config.yaml` turns them off.\n\n1. To stop the endpoint without stopping the node, run:\n\n   ```sh\n   logosctl call openmetrics stop\n   ```\n\n## Troubleshooting\n\n### `logosctl call openmetrics start` returns `\"result\":0`\n\nThe server did not start. The usual causes are:\n\n- The server is already running. Check with `logosctl call openmetrics getInfo`, and run `logosctl call openmetrics stop` before starting it with a new configuration.\n- Another process holds the port. Check with `ss -lntp | grep ':9464'`, then pick a free `port` in `config.json`.\n- `config.json` is not valid JSON, or `port` is missing or not between `1` and `65535`.\n\n### A module's metrics are missing from `/metrics`\n\n- Check that the module is loaded and started with `logosctl module ls --loaded` and the module's own status call, for example `logosctl call storage_module isRunning`.\n- Check that `blockchain_module` and `delivery_module` are listed with `\"format\": \"text\"`. As bare names they contribute no metrics.\n- If `blockchain_module` reports nothing, run `logosctl call blockchain_module collectOpenMetricsText`. An empty result means `user_config.yaml` sets `tracing.metrics.open_metrics.enabled` to `false`, the module was not restarted after the change, or the node is not running. Repeat [Step 2](#step-2-check-the-blockchain-nodes-metrics).\n"},{"id":"build-an-app/scaffold/about-logos-scaffold","title":"About Logos Scaffold","summary":"Understand what Logos Scaffold manages and when to reach for it.","section":"build-an-app","doc_type":"concept","url":"https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold","markdown_url":"https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md","permalink":"/build-an-app/scaffold/about-logos-scaffold","source":"https://github.com/logos-co/logos-docs/blob/main/docs/build-an-app/scaffold/about-logos-scaffold.md","headings":[{"depth":2,"text":"What Logos Scaffold covers"},{"depth":2,"text":"Project layout"},{"depth":2,"text":"Profiles"},{"depth":2,"text":"Pinned versions"},{"depth":2,"text":"What Logos Scaffold does not do"},{"depth":2,"text":"Related documentation"}],"markdown":"# About Logos Scaffold\n\n#### Understand what Logos Scaffold manages and when to reach for it.\n\n[Logos Scaffold](https://github.com/logos-co/scaffold) is a command-line tool for developers who build on Logos. It bootstraps projects, pins and builds the toolchain a project depends on, and drives the repetitive parts of the development loop: building [`.lgx`](https://docs.logos.co/get-started/glossary.md#lgx) packages, installing them, and launching isolated [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) instances to test them.\n\nScaffold ships two binaries with identical behaviour: `logos-scaffold` and the shorter alias `lgs`. Use either.\n\n> **Info**\n>\n> The repository is [`logos-co/scaffold`](https://github.com/logos-co/scaffold); the crate and the binary are named `logos-scaffold`, and the crate is published on [crates.io](https://crates.io/crates/logos-scaffold). The `logos-co/logos-scaffold` URL redirects to the same repository. This section is accurate for `logos-scaffold` **0.4.0** built from `master` at commit `d4b15ff`, which supports Basecamp **0.3.0** only. That commit adds the reproducible Docker guest build to the 0.4.0 release.\n\n## What Logos Scaffold covers\n\nScaffold spans two workflows that a Logos project can use independently or together.\n\n| Workflow | Commands | What it does |\n|:---|:---|:---|\n| [LEZ](https://docs.logos.co/get-started/glossary.md#lez) programs | `new` (alias `create`), `init`, `setup`, `build`, `deploy`, `localnet`, `test-node`, `wallet`, `spel`, `run` | Bootstraps a project from the `default` or `lez-framework` template, syncs and builds the pinned LEZ and `spel` binaries, runs a local sequencer, funds wallets, and deploys guest programs. |\n| Basecamp modules | `basecamp setup`, `modules`, `install`, `launch`, `develop`, `build`, `build-portable`, `run`, `doctor`, `paths`, `docs` | Pins and builds Basecamp and [`lgpm`](https://docs.logos.co/get-started/glossary.md#lgpm), captures which modules a project installs, builds their `.lgx` packages, and launches profile-isolated Basecamp instances. |\n| Diagnostics | `doctor`, `report`, `completions` | Reports environment health, bundles redacted logs and state for a bug report, and prints shell completions. |\n\nBoth workflows are also exposed as a typed Rust API under `logos_scaffold::api`, so tests and dev tooling can drive a scaffold-managed project without shelling out to the CLI and parsing text.\n\nEach workflow has a one-command inner loop:\n\n- **LEZ programs:** `lgs run` chains build → IDL → localnet → wallet topup → deploy → post-deploy hooks. Steps 4 and 5 can be skipped per profile (`topup = false`, `deploy = false`) for projects that fund or deploy themselves.\n- **Basecamp modules:** `lgs basecamp launch <profile>` rebuilds, reinstalls, and starts a clean instance.\n\n> **Info**\n>\n> Pick the guide that matches what you are building: [Write and deploy an LEZ program with `logos-scaffold`](https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold.md) for guest programs, or [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md) for `.lgx` modules that run in Basecamp.\n\n## Project layout\n\nScaffold works on a project directory that contains a `scaffold.toml` file at its root, and project-scoped commands such as `setup`, `doctor`, and the `basecamp` workflow will not run outside one. The exceptions are `lgs new`, which creates a project, `lgs init`, which adds `scaffold.toml` to an existing one, and `lgs basecamp docs`, which prints the module-project contract from anywhere.\n\n| Path | Purpose |\n|:---|:---|\n| `scaffold.toml` | The project's configuration: pinned repositories, the captured module set, run profiles, and per-profile launch settings. Hand-editable. |\n| `.scaffold/state/` | Resolved binary paths, pin-derived metadata, and the default wallet address in `wallet.state`. |\n| `.scaffold/logs/` | Timestamped build and install logs. |\n| `.scaffold/wallet/` | The project-local wallet home used by `wallet`, `deploy`, and `run`. |\n| `.scaffold/basecamp/profiles/<profile>/` | Per-profile Basecamp state: config, data, and cache directories for one instance. |\n| `.scaffold/basecamp/lgx/`, `.scaffold/basecamp/portable/` | Symlinks to `lgs basecamp build` output, one directory per variant, named `<NN>-<module_name>.lgx` in dependency order. |\n\nScaffold appends `.scaffold` to the project's `.gitignore`, so none of this working state is committed. `scaffold.toml` is meant to be committed: `basecamp modules` records in-project module sources as relative `path:./<dir>#lgx` references so the file stays portable across checkouts.\n\nTwo things live outside the project directory:\n\n- **The cache root** holds the pinned LEZ, `spel`, and Basecamp checkouts and their builds, shared by every project on the machine. It defaults to `~/Library/Caches/logos-scaffold` on macOS and `$XDG_CACHE_HOME/logos-scaffold` (usually `~/.cache/logos-scaffold`) on Linux. Point it elsewhere with `--cache-root` on `lgs new`, or with the `LOGOS_SCAFFOLD_CACHE_ROOT` environment variable, which every command honours.\n- **The Basecamp runtime directory** of each profile, `/tmp/lgs-<project-hash>-<profile>` by default, holds the Unix sockets [modules](https://docs.logos.co/get-started/glossary.md#module) open while Basecamp runs. See [Runtime directory](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#runtime-directory).\n\n> **Warning**\n>\n> Everything under `.scaffold/wallet/` is development-only key material, unlocked by a deterministic local password unless you set `LOGOS_SCAFFOLD_WALLET_PASSWORD`. Never point it at real funds.\n\n## Profiles\n\nA profile is one isolated Basecamp instance, with its own configuration, identity keys, installed modules, and message history. Scaffold seeds two profiles, `alice` and `bob`, so that peer-to-peer features can be exercised between two instances on a single machine. A project can declare further profiles under `[basecamp.profiles.<name>]` in `scaffold.toml`, each with its own environment variables, runtime directory, and log file; see the [configuration reference](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md).\n\nProfile state is always project-local under `.scaffold/basecamp/profiles/`. Scaffold does not write Basecamp state into your home directory.\n\n> **Warning**\n>\n> `lgs basecamp launch <profile>` is clean-slate by design: every launch removes the profile's state and reinstalls the captured modules before starting Basecamp. Identities, conversations, per-module persisted state (`module_data/`), and Basecamp's own `logs/` do not survive a relaunch. To keep state between restarts, run Basecamp yourself against a dedicated base directory with `--user-dir`. See [Run two instances side by side](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md#step-6-run-two-instances-side-by-side).\n\n## Pinned versions\n\nScaffold builds every tool a project depends on from a pinned commit recorded in `scaffold.toml`, so two machines building the same project get the same toolchain. The defaults at that commit are:\n\n| Component | Default pin | Override in `scaffold.toml` |\n|:---|:---|:---|\n| LEZ (sequencer and wallet) | `v0.1.2` | `[repos.lez]` |\n| `spel` | `v0.5.0` | `[repos.spel]` |\n| Guest build image | `risczero/risc0-guest-builder:r0.1.97.0` | `[build].risc0_docker_tag` |\n| Basecamp | `0.3.0` | `[repos.basecamp]` |\n| `lgpm` | The `logos-package-manager` revision that Basecamp 0.3.0 locks | `[repos.lgpm]` |\n| `delivery_module`, when a module depends on it | The newest `logos-delivery-module` commit built with `logos-module-builder` 0.3.0 | `[modules.delivery_module]` |\n\nThe Basecamp and `lgpm` pins move as a set. Basecamp reads installed modules with the same package-manager library that the `lgpm` CLI uses to write them, so bumping one without the other leaves the two disagreeing about the package format. `lgs basecamp doctor` warns when only one of the pair is at scaffold's default.\n\n`lgs basecamp setup` moves a project off pins that an earlier scaffold release wrote as its defaults. It rewrites `scaffold.toml` and prints one line per changed value. The match is on those exact revisions, so a pin you chose yourself is left alone, unless it happens to equal a retired default. To move any other pin, edit it and re-run `lgs setup` or `lgs basecamp setup`.\n\n> **Info**\n>\n> Build modules with `logos-module-builder` **0.3.0**, the release that links exactly the `logos-protocol` and `logos-cpp-sdk` revisions Basecamp 0.3.0 does. Pin it in your module's `flake.nix`, since an unpinned URL resolves to the newest release.\n\n## What Logos Scaffold does not do\n\n- **It does not replace [`logos-module-builder`](https://github.com/logos-co/logos-module-builder).** Module builder owns the Nix build of a module: it turns your source tree into `.lgx` packages. Scaffold calls that build and takes care of everything around it.\n- **It does not install anything on your `PATH`** besides its own `logos-scaffold` and `lgs`. The sequencer, wallet, `spel`, Basecamp, and `lgpm` binaries it builds stay project-local; reach them through `lgs wallet -- …`, `lgs spel -- …`, and the `basecamp` subcommands.\n- **It does not hot-reload a running Basecamp.** A rebuilt module reaches Basecamp only after it is reinstalled and Basecamp restarts. For live QML edits, use the `ui-dev` target of `logos-module-builder` in the standalone app; see [Iterate faster on a single module](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md#iterate-faster-on-a-single-module).\n\n## Related documentation\n\n- [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md)\n- [Troubleshoot Logos module development with Basecamp](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md)\n- [Logos Scaffold Basecamp configuration reference](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md)\n- [Write and deploy an LEZ program with `logos-scaffold`](https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold.md)\n- [Build and run a Logos core module](https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module.md)\n- [Install Logos Basecamp](https://docs.logos.co/basecamp/install-logos-basecamp.md)\n- [Scaffold command reference](https://github.com/logos-co/scaffold/blob/master/docs/commands.md) and [`scaffold.toml` run configuration](https://github.com/logos-co/scaffold/blob/master/docs/configuration.md) in the scaffold repository\n"},{"id":"build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold","title":"Develop a Logos module with Logos Scaffold","summary":"Build a module, install it into Basecamp, and iterate on it.","section":"build-an-app","doc_type":"procedure","url":"https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold","markdown_url":"https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md","permalink":"/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold","source":"https://github.com/logos-co/logos-docs/blob/main/docs/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install Logos Scaffold"},{"depth":2,"text":"Step 2: Prepare the module project"},{"depth":2,"text":"Step 3: Build and install the module"},{"depth":2,"text":"Step 4: Launch Basecamp"},{"depth":2,"text":"Step 5: Iterate on a source change"},{"depth":3,"text":"Iterate faster on a single module"},{"depth":3,"text":"Iterate without scaffold"},{"depth":2,"text":"Step 6: Run two instances side by side"},{"depth":3,"text":"Option A—Scaffold profiles"},{"depth":3,"text":"Option B—Your own Basecamp with `--user-dir`"},{"depth":2,"text":"Step 7: Test against a released Basecamp"},{"depth":2,"text":"Next steps"}],"markdown":"# Develop a Logos module with Logos Scaffold\n\n#### Build a module, install it into Basecamp, and iterate on it.\n\n> **Tip: Version**\n>\n> This document is accurate for `logos-scaffold` **0.4.0** built from `master` at commit `d4b15ff`, and Basecamp **0.3.0**.\n\n[Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md) drives the loop a [module](https://docs.logos.co/get-started/glossary.md#module) author repeats often: build the [`.lgx`](https://docs.logos.co/get-started/glossary.md#lgx) package, install it into a [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) instance, restart Basecamp, and check the result. This guide describes that loop, including running two instances side by side to exercise peer-to-peer features.\n\n> **Info: Prerequisites**\n>\n>\n> - Linux (x86_64 or aarch64) or macOS (arm64 or x86_64). Scaffold is Unix-only.\n> - [Nix](https://nixos.org/download.html) with flakes enabled. Required by every `basecamp` subcommand.\n> - `git`, and Rust 1.81 or newer with `cargo`, to install scaffold.\n> - The Unix process helpers `lsof`, `ps`, and `kill`, which scaffold uses to track the processes it starts.\n> - A module project that exposes `packages.<system>.lgx` from a `flake.nix`, built with [`logos-module-builder`](https://github.com/logos-co/logos-module-builder) **0.3.0**, the release whose SDK matches Basecamp 0.3.0. See [Pinned versions](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#pinned-versions).\n>     - If you do not have a module yet, start from a 0.3.0 template and pin the builder in the generated `flake.nix`, whose URL the template leaves unpinned:\n>\n>         ```bash\n>         mkdir my_ui && cd my_ui && git init\n>         nix flake init -t github:logos-co/logos-module-builder/0.3.0#ui-qml\n>         sed -i.bak 's#github:logos-co/logos-module-builder\"#github:logos-co/logos-module-builder/0.3.0\"#' flake.nix\n>         rm flake.nix.bak\n>         git add -A\n>         ```\n>\n>     - The `ui-qml` template produces a QML-only module that shows up in the Basecamp sidebar, with the 256×256 icon builder 0.3.x requires in `src/icons/icon.png`. Set `name` and `display_name` in `metadata.json`, and move `Main.qml` into a sub-directory such as `qml/` (updating `view`) if the module will have more than one QML file.\n>     - The same release has `default` (a minimal [core module](https://docs.logos.co/get-started/glossary.md#core-module)), `with-external-lib`, `ui-qml-backend`, `rust`, and `rust-with-external-lib` templates.\n> - About 5 GB of free memory for the first `lgs basecamp setup`, which evaluates the Basecamp 0.3.0 flake before building anything. On a smaller machine, fetch the prebuilt Basecamp first as described in [Basecamp setup is killed](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md#basecamp-setup-is-killed).\n> - A graphical environment. Basecamp is a desktop application.\n\n## What to expect\n\n- You can build your module and run it in isolated Basecamp profiles.\n- You can apply a source change and see it in a running Basecamp.\n- You can run two Basecamp instances at once and exercise peer-to-peer features between them.\n\n## Step 1: Install Logos Scaffold\n\n1. Install both binaries from the scaffold repository, pinned to the commit this guide was tested with:\n\n   ```bash\n   cargo install --locked --git https://github.com/logos-co/scaffold --rev d4b15ff logos-scaffold\n   ```\n\n   This installs `logos-scaffold` and its shorter alias `lgs`. They are functionally identical; this guide uses `lgs`.\n\n   The `basecamp` commands behave the same in the `logos-scaffold` 0.4.0 release on [crates.io](https://crates.io/crates/logos-scaffold), so `cargo install logos-scaffold` also works for this guide. Use the pinned commit if you also follow [Write and deploy an LEZ program with `logos-scaffold`](https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold.md).\n\n1. Confirm the install:\n\n   ```bash\n   lgs --version\n   ```\n\n> **Tip**\n>\n> `lgs completions bash` and `lgs completions zsh` print shell completion scripts that cover both binary names. Each subcommand also documents copy-paste examples under `--help`.\n\n## Step 2: Prepare the module project\n\nRun these commands from the root of your module project.\n\n1. Create `scaffold.toml` if the project does not have one:\n\n   ```bash\n   lgs init\n   ```\n\n   - `init` also creates `.scaffold/`, appends `.scaffold` to `.gitignore`, and installs AI-assistant instructions under `.claude/skills/`, `.cursor/rules/`, and `AGENTS.md`.\n   - A `scaffold.toml` from an older schema is migrated in place, with a `scaffold.toml.bak` next to it.\n\n   > **Warning**\n   >\n   > `init` overwrites an existing `AGENTS.md` at the project root without prompting, and unlike `scaffold.toml` it leaves no `.bak` copy. If your project already has an `AGENTS.md`, commit it or copy it somewhere safe before you run `init`, then merge your content back into the new file.\n\n1. Pin and build Basecamp and [`lgpm`](https://docs.logos.co/get-started/glossary.md#lgpm), and seed the `alice` and `bob` profiles:\n\n   ```bash\n   lgs basecamp setup\n   ```\n\n   - The first run evaluates and builds for a long time; later runs reuse the build and print `basecamp (.#app) already built at /nix/store/…; skipping nix build`.\n   - Neither binary lands on your `PATH`; both live under scaffold's [cache root](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#project-layout). Build logs go to `.scaffold/logs/<timestamp>-setup-*.log`.\n\n   If the build fails with `signal: 9 (SIGKILL)`, the machine ran out of memory. See [Basecamp setup is killed](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md#basecamp-setup-is-killed).\n\n1. Capture the modules to install, then review what was captured:\n\n   ```bash\n   lgs basecamp modules\n   lgs basecamp modules --show\n   ```\n\n   `basecamp modules` discovers the `flake.nix` at the project root that exposes `packages.<system>.lgx`, or, when the root has none, every immediate sub-directory flake that does. It resolves the dependencies each module declares in `metadata.json` and writes the result to `[modules]` in `scaffold.toml`:\n\n   ```toml\n   [modules.tictactoe]\n   flake = \"path:./tictactoe#lgx\"\n   role = \"project\"\n\n   [modules.delivery_module]\n   flake = \"github:logos-co/logos-delivery-module/<rev>#lgx\"\n   role = \"dependency\"\n   ```\n\n   - `role = \"project\"` marks a module you build locally, `role = \"dependency\"` a runtime companion. The keys are described in the [configuration reference](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#modulesname).\n   - The table is hand-editable, and re-running `basecamp modules` never overwrites an existing entry.\n   - Modules that Basecamp bundles itself, such as `package_manager`, are never captured.\n\n   **Expected result:** `--show` lists your module under `project_sources`, for example `hello_ui = path:.#lgx`.\n\n> **Info**\n>\n> `lgs basecamp docs` prints the full module-project contract, including the dependency-resolution rules. It works outside a scaffold project too, so you can read the contract before running `lgs init`.\n\n## Step 3: Build and install the module\n\n1. Build every captured module and install the results into both profiles:\n\n   ```bash\n   lgs basecamp install\n   ```\n\n   - Dependencies are built and installed first, then project modules; the first failure stops the run.\n   - `lgpm` validates each package's structure and content hashes as it installs it.\n   - Output goes to a timestamped log under `.scaffold/logs/`. Pass `--print-output` to stream the Nix output instead.\n\n   **Expected result:** `install complete`.\n\n1. Check the project's Basecamp health:\n\n   ```bash\n   lgs basecamp doctor\n   ```\n\n   `doctor` checks that `setup` built Basecamp and `lgpm` and seeded the profiles. It also reports the pin set, the captured modules, the platform variant of each installed core module, and drift between `[modules]` and what the project would capture today. Add `--json` for machine-readable output.\n\n   **Expected result:** `Doctor status: Ready`.\n\n## Step 4: Launch Basecamp\n\n1. Start a profile:\n\n   ```bash\n   lgs basecamp launch alice\n   ```\n\n   `launch` stops the instance this profile started last time, removes the profile's data and cache, rebuilds and reinstalls every captured module, and starts Basecamp with the profile's [launch environment](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#launch-environment). It fails before deleting anything if `[modules]` is empty or the profile's runtime directory cannot be used.\n\n   - Before starting Basecamp, it prints a variant check of the installed core modules, such as `launch: profile alice has 3 module(s); all linux-amd64-dev variants present ✓`. The count covers core modules only, so a project of QML-only modules reports `0 module(s)`.\n   - Add `--log-file` to also write the output to `.scaffold/basecamp/profiles/<profile>/basecamp.log`, or `--log-file=PATH` to pick the file.\n\n1. Confirm your module appears in the Basecamp window. A [UI module](https://docs.logos.co/get-started/glossary.md#ui-module) gets an entry in the sidebar, showing its icon, or its initials when it ships none; click it to open the module.\n\n> **Warning**\n>\n> Every launch starts from a clean profile, so identity keys, conversations, other in-app state, per-module state in `module_data/`, and Basecamp's own `logs/` are discarded. That is what makes runs reproducible. If you need state that survives a restart, run Basecamp yourself with `--user-dir`, as in [Step 6](#step-6-run-two-instances-side-by-side).\n\nTo see the directories and environment a profile resolves without launching it, run `lgs basecamp paths alice --json`. To add your own variables to the launch, see [Launch environment](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#launch-environment).\n\n## Step 5: Iterate on a source change\n\nBuilding a module does not update a running Basecamp instance. Basecamp loads modules once, at startup, from the copies installed in its base directory, so a rebuilt `.lgx` reaches a running instance only after it is reinstalled there and Basecamp restarts.\n\n1. Edit your source. If you add a file, stage it before the next build:\n\n    ```bash\n    git add -A\n    ```\n\n    > **Warning**\n    >\n    > `nix build .#lgx`, `lgs basecamp build`, and `build-portable` only see files that git tracks, so a new file you have not staged is silently left out of the package they produce. `lgs basecamp install` and `launch` build from the directory instead and do include it, which means a module can work in your profiles while the package you ship is missing a `qmldir`, a QML file, or a configuration file. Stage every new file before you build a package to share. See [A new file is missing from the built package](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md#a-new-file-is-missing-from-the-built-package).\n\n1. Relaunch every profile you are testing. `launch` rebuilds the module, reinstalls it into the profile, and starts a fresh instance:\n\n    ```bash\n    lgs basecamp launch alice\n    ```\n\n    - Repeat it for each profile. Relaunching `alice` leaves a running `bob` on the previous build.\n\n### Iterate faster on a single module\n\nThese loops skip a full Basecamp launch while you work on one module. Use them for iteration, then go back to Basecamp to verify the module in its real host.\n\n- `lgs basecamp run <module>` runs a captured module in its own standalone app through `nix run`, without the rest of the stack.\n- For QML-only changes, `logos-module-builder` 0.3.0 has a `ui-dev` target that reloads QML from your working tree on every save. It prints `hot-reloading QML from <project>/qml`, then `QML reloaded …` after each edit:\n\n  ```bash\n  nix build .#ui-dev\n  ./result/bin/run-logos-standalone-ui\n  ```\n\n- `lgs basecamp develop <module>` opens that module's Nix dev shell when you need to build by hand.\n- `lgs basecamp build` builds the captured project modules without installing them, and links the packages under `.scaffold/basecamp/lgx/` and `.scaffold/basecamp/portable/` in load order.\n\n### Iterate without scaffold\n\nTo do the same by hand, rebuild the `.lgx`, install it into **every** base directory you test against with an `lgpm` built from the same revision as your Basecamp, and restart Basecamp:\n\n```bash\nnix build .#lgx\n\nlgpm --modules-dir /tmp/basecamp-a/modules \\\n     --ui-plugins-dir /tmp/basecamp-a/plugins \\\n     install --file result/logos-<module-name>-module.lgx\n\n# Then close and restart the Basecamp instance that uses /tmp/basecamp-a.\n```\n\nThe Package Manager UI inside Basecamp installs into the base directory of the instance you click in; the restart requirement is the same.\n\n## Step 6: Run two instances side by side\n\nPeer-to-peer features need two instances, each with its own base directory and its own runtime directory. Choose one of the two options below.\n\n### Option A—Scaffold profiles\n\n1. In one terminal at the project root, launch the first profile:\n\n    ```bash\n    lgs basecamp launch alice\n    ```\n\n1. In a second terminal at the project root, launch the second profile:\n\n    ```bash\n    lgs basecamp launch bob\n    ```\n\n    **Expected result:** two Basecamp windows, each with its own data under `.scaffold/basecamp/profiles/<profile>/`, its own runtime directory under `/tmp`, and `LOGOS_PROFILE` set to the profile name. If your module implements peer communication, an action in `alice` should be observable in `bob`. The `ui-qml` template only logs its button clicks locally, so with it you can check isolation but not peer communication.\n\nTo add more profiles, such as a `maker` and a `taker`, declare them under `[basecamp.profiles.<name>]` in `scaffold.toml`; see [`[basecamp.profiles.<name>]`](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#basecampprofilesname). Unknown profiles are seeded on first launch.\n\n### Option B—Your own Basecamp with `--user-dir`\n\n1. Install your module into two base directories, one per instance, as in [Iterate without scaffold](#iterate-without-scaffold), with `/tmp/basecamp-a` and `/tmp/basecamp-b` as the base directories.\n\n1. Start each instance on its own base directory and its own short temporary directory:\n\n    ```bash\n    mkdir -p /tmp/bc-a /tmp/bc-b\n    TMPDIR=/tmp/bc-a XDG_RUNTIME_DIR=/tmp/bc-a LogosBasecamp --user-dir /tmp/basecamp-a &\n    TMPDIR=/tmp/bc-b XDG_RUNTIME_DIR=/tmp/bc-b LogosBasecamp --user-dir /tmp/basecamp-b &\n    ```\n\n    - Keep the temporary directories short: module sockets live there, and socket paths are capped at about 100 bytes.\n    - The executable is `./result/bin/LogosBasecamp` for a Nix build, `./logos-basecamp.AppImage` for the Linux release, or the application bundle on macOS.\n    - Each base directory holds its own copy of the module. After a rebuild, reinstall into both and restart both.\n\nState in these directories survives restarts. Without `--user-dir`, Basecamp picks a default base directory that differs between development and release builds; see [Base directory](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#base-directory).\n\n## Step 7: Test against a released Basecamp\n\nProfiles run the Basecamp build that `basecamp setup` pinned. To test your module against a released AppImage or DMG instead, build the portable variant and load it by hand.\n\n1. Build the portable variant of every project module:\n\n    ```bash\n    lgs basecamp build --variant lgx-portable\n    ```\n\n    - `lgs basecamp build-portable` is the back-compatible alias.\n    - Add `--module <name>` to build one module instead of all of them.\n    - `role = \"dependency\"` entries are skipped, because the released build ships its own copies.\n    - A flake that does not expose `lgx-portable` fails the build instead of falling back to `lgx`.\n\n    **Expected result:** scaffold prints the built packages in load order and links them into `.scaffold/basecamp/portable/` as `<NN>-<module_name>.lgx`.\n\n1. In the released Basecamp instance, install each package through its install button, in the printed order.\n\nTo run a portable Basecamp instance in your scaffold profiles instead, set `attr` in `[repos.basecamp]`; see [`[repos.basecamp]` and `[repos.lgpm]`](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#reposbasecamp-and-reposlgpm).\n\n## Next steps\n\n- [Troubleshoot Logos module development with Basecamp](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md)\n- [Logos Scaffold Basecamp configuration reference](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md)\n- [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md)\n- [Install and load a module in Basecamp](https://docs.logos.co/basecamp/install-and-load-module-in-basecamp.md)\n- [Write and deploy an LEZ program with `logos-scaffold`](https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold.md), for the other half of scaffold's surface\n"},{"id":"build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp","title":"Troubleshoot Logos module development with Basecamp","summary":"Diagnose the failures that show up while iterating on a module.","section":"build-an-app","doc_type":"troubleshooting","url":"https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp","markdown_url":"https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md","permalink":"/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md","headings":[{"depth":2,"text":"Symptom index"},{"depth":2,"text":"Basecamp setup is killed"},{"depth":2,"text":"Setup was built for another pin"},{"depth":2,"text":"A module depends on `main_ui`"},{"depth":2,"text":"A dependency cannot be resolved"},{"depth":2,"text":"A sibling sub-flake override is ignored"},{"depth":2,"text":"A new file is missing from the built package"},{"depth":2,"text":"Install fails with `Missing content hashes in manifest`"},{"depth":2,"text":"Install fails with `Forbidden root entry: assets`"},{"depth":2,"text":"Install fails inside a Nix build"},{"depth":2,"text":"An installed module lacks the platform variant"},{"depth":2,"text":"Launch finds no modules"},{"depth":2,"text":"Launch refuses the runtime directory"},{"depth":2,"text":"Module loading aborts with a socket path error"},{"depth":2,"text":"Builds fail after the first launch"},{"depth":2,"text":"Your change is not visible after a rebuild"},{"depth":2,"text":"An installed module is missing"},{"depth":2,"text":"A QML type or singleton resolves to the wrong module"},{"depth":2,"text":"Two instances collide"},{"depth":2,"text":"Profiles share state on macOS"},{"depth":2,"text":"Doctor reports drift"},{"depth":2,"text":"Collect diagnostics"},{"depth":2,"text":"Related documentation"}],"markdown":"# Troubleshoot Logos module development with Basecamp\n\n#### Diagnose the failures that show up while iterating on a module.\n\n> **Tip: Version**\n>\n> This document is accurate for `logos-scaffold` **0.4.0** built from `master` at commit `d4b15ff`, and Basecamp **0.3.0**.\n\nMost problems in the [module](https://docs.logos.co/get-started/glossary.md#module) development loop share a shape: the build succeeds, no error is printed, and the running application does not do what the source says it should. This page maps those symptoms to their causes.\n\nThe commands here assume [Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md) and a module project set up as described in [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md).\n\n## Symptom index\n\nSections follow the development loop: setup, capturing modules, building and installing, launching, running, and platform-specific issues.\n\n| Symptom | Section |\n|:---|:---|\n| `lgs basecamp setup` fails with `signal: 9 (SIGKILL)` | [Basecamp setup is killed](#basecamp-setup-is-killed) |\n| `basecamp was set up for pin … but scaffold.toml now pins …` | [Setup was built for another pin](#setup-was-built-for-another-pin) |\n| `module dependencies on packages basecamp no longer ships` | [A module depends on `main_ui`](#a-module-depends-on-main_ui) |\n| `unresolved module dependencies (declared in metadata.json but not resolvable)` | [A dependency cannot be resolved](#a-dependency-cannot-be-resolved) |\n| A sibling sub-flake builds from the wrong source | [A sibling sub-flake override is ignored](#a-sibling-sub-flake-override-is-ignored) |\n| A file you just added is absent at runtime | [A new file is missing from the built package](#a-new-file-is-missing-from-the-built-package) |\n| `Missing content hashes in manifest` during install | [Install fails with `Missing content hashes in manifest`](#install-fails-with-missing-content-hashes-in-manifest) |\n| `Forbidden root entry: assets` during install | [Install fails with `Forbidden root entry: assets`](#install-fails-with-forbidden-root-entry-assets) |\n| `no 'main' field in metadata.json` during install | [Install fails inside a Nix build](#install-fails-inside-a-nix-build) |\n| `has no variant for this stack` during install | [An installed module lacks the platform variant](#an-installed-module-lacks-the-platform-variant) |\n| `no modules captured` when launching | [Launch finds no modules](#launch-finds-no-modules) |\n| `refusing to use runtime dir …` or `cannot restrict permissions on runtime dir …` | [Launch refuses the runtime directory](#launch-refuses-the-runtime-directory) |\n| `Unix socket path too long (122 >= 104)` | [Module loading aborts with a socket path error](#module-loading-aborts-with-a-socket-path-error) |\n| `file '…/logos_…' has an unsupported type` during a build | [Builds fail after the first launch](#builds-fail-after-the-first-launch) |\n| A rebuilt module behaves as if nothing changed | [Your change is not visible after a rebuild](#your-change-is-not-visible-after-a-rebuild) |\n| A module you installed is missing from Basecamp | [An installed module is missing](#an-installed-module-is-missing) |\n| `Invalid null URL`, or a QML singleton holding the wrong values | [A QML type or singleton resolves to the wrong module](#a-qml-type-or-singleton-resolves-to-the-wrong-module) |\n| Two instances share identity or crash together | [Two instances collide](#two-instances-collide) |\n| Two macOS profiles share modules and identity despite isolation | [Profiles share state on macOS](#profiles-share-state-on-macos) |\n| The macOS window opens but the UI never renders | [Profiles share state on macOS](#profiles-share-state-on-macos) |\n| `basecamp doctor` reports drift | [Doctor reports drift](#doctor-reports-drift) |\n\n## Basecamp setup is killed\n\n**Symptom.** `lgs basecamp setup` fails after several minutes with `error: building basecamp (.#app) failed with signal: 9 (SIGKILL)`. The setup log under `.scaffold/logs/` ends mid-way, often right after an `evaluation warning:` or `copying path` line, with no build error.\n\n**Cause.** The machine ran out of memory and the kernel killed Nix. Evaluating the [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) 0.3.0 flake, before anything is built, peaks above 4.8 GB of resident memory, because its lock file pins thousands of inputs. Scaffold reports only the signal.\n\n**Fix.** Free memory and run `lgs basecamp setup` again: close other large applications, stop Basecamp instances, and do not run another large build at the same time. Tuning the parallelism or garbage collector of Nix does not reduce the peak noticeably.\n\nIf the machine still cannot evaluate the flake, skip the evaluation. Basecamp's own CI publishes the 0.3.0 build to `cache.nix.logos.co/ci`, and `setup` reuses a build it finds for the pinned commit. On x86_64 Linux, fetch it into your scaffold [cache root](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#project-layout) and run `setup` as usual:\n\n```bash\nOUT=/nix/store/yf8mbqrxkgw0kjyfgs8f4491rjx9vypz-logos-basecamp-0.3.0\nnix-store -r \"$OUT\" --option extra-substituters https://cache.nix.logos.co/ci --option require-sigs false\nPIN=bbe5da0e038ef19095690f4164a8ddaab0915821   # [repos.basecamp].pin in scaffold.toml\nCACHE=~/.cache/logos-scaffold                    # or $LOGOS_SCAFFOLD_CACHE_ROOT\nmkdir -p \"$CACHE/basecamp/$PIN\"\nnix build \"$OUT\" --out-link \"$CACHE/basecamp/$PIN/app-result\"\nlgs basecamp setup\n```\n\n`setup` then prints `basecamp (.#app) already built at /nix/store/…; skipping nix build` and goes on to build `lgpm` and seed the profiles. The store path is specific to the 0.3.0 pin and to x86_64 Linux, and `require-sigs false` accepts unsigned paths from that cache, so use it only if you trust it. On macOS the default cache root is `~/Library/Caches/logos-scaffold`.\n\n## Setup was built for another pin\n\n**Symptom.** `lgs basecamp install` or `launch` stops with `basecamp was set up for pin <old> but scaffold.toml now pins <new>; run: logos-scaffold basecamp setup`, and `lgs basecamp doctor` shows the same message as a `basecamp build pin` warning.\n\n**Cause.** The Basecamp and `lgpm` binaries recorded for the project were built for a different `[repos.basecamp].pin` than the one `scaffold.toml` has now, for example after you edited the pin or pulled a change to `scaffold.toml`.\n\n**Fix.** Run `lgs basecamp setup` to build the pinned revision, then repeat the command.\n\n## A module depends on `main_ui`\n\n**Symptom.** `lgs basecamp modules` fails with `module dependencies on packages basecamp no longer ships`, naming `main_ui`.\n\n**Cause.** Since 0.3.0 the Basecamp shell UI is part of the app, not a [package](https://docs.logos.co/get-started/glossary.md#package), so no module can depend on it. The same applies to the 0.1.x packages `counter`, `counter_qml`, and `webview_app`. Listed under `optional_dependencies` instead, such an entry is skipped with a note.\n\n**Fix.** Remove the entry from `dependencies` in that module's `metadata.json` and rebuild the module with `logos-module-builder` 0.3.0.\n\n## A dependency cannot be resolved\n\n**Symptom.** `lgs basecamp modules` fails with `unresolved module dependencies (declared in metadata.json but not resolvable)`, naming each dependency it could not resolve to a flake reference.\n\n**Cause.** A module's `metadata.json` declares a dependency that is not already in `[modules]`, is not one of the modules Basecamp ships with, is not an input of the declaring module's `flake.lock`, and is not in scaffold's built-in table. Scaffold fails rather than dropping it silently, because a missing runtime dependency surfaces much later as an unexplained failure inside Basecamp.\n\n**Fix.** Either declare the dependency as a flake input in the module that needs it, which is the better option because the lock then pins the exact revision your module was built against:\n\n```nix\ninputs.delivery_module.url = \"github:logos-co/logos-delivery-module/<rev>\";\n```\n\nOr add the entry to `scaffold.toml` by hand:\n\n```toml\n[modules.delivery_module]\nflake = \"github:logos-co/logos-delivery-module/<rev>#lgx\"\nrole = \"dependency\"\n```\n\nHand-written entries survive every later `basecamp modules` run.\n\nA related case: scaffold derives the module name for a remote flake reference from the repository slug and prints a note saying so. If the guess is wrong, correct the key in `scaffold.toml`. Re-runs preserve it.\n\n## A sibling sub-flake override is ignored\n\n**Symptom.** A multi-flake project builds a sibling module from its pinned revision instead of your working tree, so local changes to the sibling do not appear.\n\n**Cause.** Scaffold rewrites `path:../<sibling>` inputs to point at the working tree, but it reads `flake.nix` line by line. It recognises `<name>.url = \"path:../<sibling>\";` and `inputs.<name>.url = \"path:../<sibling>\";`. A declaration split across lines inside a nested attribute set is not detected.\n\n**Fix.** Flatten the declaration to a single line:\n\n```nix\n# Detected\ntictactoe_core.url = \"path:../tictactoe\";\n\n# Not detected\ninputs.tictactoe_core = {\n  url = \"path:../tictactoe\";\n};\n```\n\nOnly `path:../<sibling>` inputs are rewritten. `path:./sub`, `github:`, and `git+` references pass through unchanged.\n\n## A new file is missing from the built package\n\n**Symptom.** You add a file, the build succeeds, and at runtime the file is not there. A `qmldir` is ignored, an asset does not load, or a configuration file falls back to defaults. Often the module works in a profile started with `lgs basecamp launch` but not from a package built with `lgs basecamp build`, `build-portable`, or `nix build .#lgx`.\n\n**Cause.** A flake with `src = ./.` referenced as `.` builds from the git tree of your project, and files that git does not track are not part of that tree. `nix build .#lgx`, `lgs basecamp build`, and `build-portable` build that way. `lgs basecamp install` and `launch` build the same flake by path (`nix build path:<dir>#lgx`), which copies the whole directory and does include files git does not track. Nothing warns you: each build succeeds, and only the git-based ones produce a `.lgx` that is quietly incomplete.\n\n**Fix.** Stage new files before building:\n\n```bash\ngit add -A\nlgs basecamp build\n```\n\nThe files must be tracked, not necessarily committed. Staging is enough.\n\nMake this the reflex after adding any file, and check before a build that surprises you:\n\n```bash\ngit status --short     # anything marked ?? is invisible to the build\n```\n\n## Install fails with `Missing content hashes in manifest`\n\n**Symptom.** `lgs basecamp install` or `launch` fails when installing a package, with `Package validation failed: Missing content hashes in manifest`, followed by a scaffold hint about rebuilding the package.\n\n**Cause.** The `lgpm` that scaffold pins for Basecamp 0.3.0 validates each package's structure and Merkle content hashes on install. Packages built by `logos-module-builder` before 0.2.0, including every `tutorial-v1`-era package, carry no hashes. The same applies to a dependency pinned to an old revision, for example `delivery_module` at a `tutorial-v1-compat` commit.\n\n**Fix.** Rebuild the module with `logos-module-builder` 0.3.0, or bundle your `#lib` output with a matching [`nix-bundle-lgx`](https://github.com/logos-co/nix-bundle-lgx). For a dependency, move its `[modules.<name>]` entry, or its input in your `flake.nix`, to a release built with that tooling.\n\nDowngrading `[repos.lgpm].pin` does not help: the Basecamp that the pin set builds embeds the same validating library, and it is the one that reads the installed modules. See [Pinned versions](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#pinned-versions).\n\n## Install fails with `Forbidden root entry: assets`\n\n**Symptom.** `lgs basecamp install` or `launch` fails with `Package validation failed: Forbidden root entry: assets`.\n\n**Cause.** The module was built with `logos-module-builder` 0.3.x, which writes the package icon to an `assets/` directory at the package root, but the project pins an older `lgpm` that does not accept that layout. That happens when `[repos.basecamp]` and `[repos.lgpm]` still point at Basecamp 0.2.3 or earlier.\n\n**Fix.** Run `lgs basecamp doctor` to see the pin set. If it reports a retired scaffold default, `lgs basecamp setup` moves both pins to the Basecamp 0.3.0 set. If you pinned the older revisions yourself, move both `[repos.basecamp]` and `[repos.lgpm]` to the 0.3.0 set, then run `setup` and reinstall.\n\n## Install fails inside a Nix build\n\n**Symptom.** `lgs basecamp install` fails inside `nix build` with an error such as `no 'main' field in metadata.json`, while `cd <sub-flake> && nix build .#lgx` succeeds.\n\n**Cause.** A sub-flake pulls in a module that itself depends on `logos-module-builder`. Without a `follows` entry, its `flake.lock` ends up with two `logos-module-builder` nodes: your pin, and a second one dragged in transitively. A direct build in that directory uses the sub-flake's own lock and never reads the extra node. When scaffold builds with `--override-input`, the stale node wins.\n\n**Fix.** In each sub-flake that declares both `logos-module-builder` and a dependency that pulls it in, unify them:\n\n```nix\n{\n  inputs = {\n    logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.0\";\n    delivery_module.url = \"github:logos-co/logos-delivery-module/<pinned-rev>\";\n\n    # Force the transitive reference onto our pin.\n    delivery_module.inputs.logos-module-builder.follows = \"logos-module-builder\";\n  };\n}\n```\n\nThen run `nix flake update` in that sub-flake and confirm the lock holds a single `logos-module-builder` revision.\n\n## An installed module lacks the platform variant\n\n**Symptom.** `lgs basecamp install` or `launch` fails with ``the `.lgx` has no variant for this stack``, followed by `lgpm`'s `Package does not contain variant for platform: linux-x86_64-dev (package provides: linux-amd64)`. With a package installed by other means, the module instead freezes the first time you click it in a development Basecamp, or a portable Basecamp drops it at start-up; `launch` then prints `… missing linux-amd64-dev variant (<module>)` instead of `all linux-amd64-dev variants present ✓`.\n\n**Cause.** The package has no variant for the platform and stack Basecamp runs on. Development builds of Basecamp need the `-dev` key, for example `linux-amd64-dev` or `darwin-arm64-dev`. Portable builds need the bare key, for example `linux-amd64`. A `.lgx` built from the `lgx` output carries `-dev` keys, and one built from `lgx-portable` carries bare keys.\n\n**Fix.** Build the variant that matches the Basecamp you run. Scaffold profiles use the `lgx` output unless `[repos.basecamp].attr` selects a portable stack. For a released AppImage or DMG, use `lgs basecamp build-portable`. For [core modules](https://docs.logos.co/get-started/glossary.md#core-module), `lgs basecamp doctor` repeats the variant check for every seeded profile.\n\n## Launch finds no modules\n\n**Symptom.** `lgs basecamp launch` fails with ``no modules captured — run `logos-scaffold basecamp modules` before launching.``\n\n**Cause.** `[modules]` in `scaffold.toml` is empty. Every launch scrubs the profile and reinstalls only what that table captures, so launching with an empty table would start a Basecamp with none of your modules.\n\n**Fix.** Capture the modules, check the result, and launch again:\n\n```bash\nlgs basecamp modules\nlgs basecamp modules --show\nlgs basecamp launch alice\n```\n\n## Launch refuses the runtime directory\n\n**Symptom.** `lgs basecamp launch` stops before building anything, with `refusing to use runtime dir <path> — it is a symlink`, `… it exists but is not a directory`, or `cannot restrict permissions on runtime dir <path> — it is owned by another user`.\n\n**Cause.** The runtime directory holds the sockets of running modules, so scaffold only uses a directory it owns and can restrict to mode `0700`. The default `/tmp/lgs-<project-hash>-<profile>` name is predictable, so on a shared machine another user can create it first.\n\n**Fix.** Remove the offending path if it is yours, or point the profile somewhere you control:\n\n```toml\n[basecamp.profiles.alice]\nruntime_dir = \"/tmp/lgs-myname-alice\"\n```\n\nThe check runs before the profile is scrubbed, so nothing is lost.\n\n## Module loading aborts with a socket path error\n\n**Symptom.** Modules fail to load and the log contains:\n\n```\n[SubprocessContainer] Unix socket path too long (122 >= 104)\n```\n\n**Cause.** Loading a module opens a Unix domain socket for it under `TMPDIR`, and the operating system caps the whole socket path at 104 bytes on macOS and 108 on Linux. A long runtime directory, for example a `runtime_dir` inside a deep project directory, uses up that budget before the socket name is appended. See [Runtime directory](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#runtime-directory).\n\n**Fix.** Point the runtime root at a short path. Scaffold's default, `/tmp/lgs-<project-hash>-<profile>`, stays well under the limit on every platform, and scaffold exports it as both `TMPDIR` and `XDG_RUNTIME_DIR`. If you configured `runtime_dir` yourself, shorten it or remove it:\n\n```toml\n[basecamp.profiles.alice]\nruntime_dir = \"/tmp/lgs-alice\"\n```\n\nWhen you launch Basecamp outside scaffold, export the variables before starting it:\n\n```bash\nexport TMPDIR=/tmp/bc-a\nexport XDG_RUNTIME_DIR=/tmp/bc-a\nmkdir -p \"$TMPDIR\"\n```\n\nKeep any override short. A project-relative or deeply nested directory can exceed the budget again once the socket name is appended.\n\n## Builds fail after the first launch\n\n**Symptom.** The first `lgs basecamp launch` works. Every later `lgs basecamp install` or `launch`, or a second profile launched while the first is running, fails inside `nix build` with:\n\n```\nerror: file '/nix/store/…-source/<runtime-dir>/logos_capability_module_<id>' has an unsupported type\n```\n\n**Cause.** The profile's runtime directory is inside the project tree. The socket name in the error depends on the Basecamp release. When the module flake is the project root, `nix build` copies that tree into the store and refuses to copy the Unix sockets a running Basecamp leaves in the runtime directory. Scaffold releases before 0.3.1 put it there by default on Linux; with 0.3.1 and later it happens only when `runtime_dir` points inside the project.\n\n**Fix.** Remove any `runtime_dir` that points inside the project, so the default `/tmp/lgs-<project-hash>-<profile>` applies. The next `launch` also clears the legacy `xdg-tmp` directory, so an affected project recovers without manual clean-up.\n\n## Your change is not visible after a rebuild\n\n**Symptom.** You edit a source file, run `nix build .#lgx`, the build succeeds, and the running Basecamp behaves exactly as before.\n\n**Cause.** Building a module produces a new package in the Nix store. It does not modify the copy already installed in a Basecamp base directory, and Basecamp resolves and loads its modules once, during startup. Nothing about a successful build reaches a process that is already running.\n\n**Fix.** Reinstall into every base directory you test against, then restart Basecamp. With scaffold, one command does both for a profile:\n\n```bash\nlgs basecamp launch alice\n```\n\nRepeat it for every profile you have open; a relaunch of `alice` leaves a running `bob` on the previous build. For the manual equivalent and faster loops, see [Iterate on a source change](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md#step-5-iterate-on-a-source-change).\n\n## An installed module is missing\n\n**Symptom.** You installed a module, launched Basecamp, and the module is not listed. Or it is present in one Basecamp window and absent in another on the same machine.\n\n**Cause.** The instances read from different base directories. Without an explicit `--user-dir`, a development build and a released build use different default directories, and every scaffold profile has its own under `.scaffold/basecamp/profiles/<profile>/`. Installing into one has no effect on an instance reading another; see [Base directory](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md#base-directory).\n\nDo not expect Basecamp's bundled modules, such as `package_manager` or `modules_state`, in a profile's `modules/` directory. Basecamp loads them from next to its own binary.\n\n**Fix.** Pass `--user-dir` explicitly whenever you run Basecamp yourself, and install into that same path:\n\n```bash\nLogosBasecamp --user-dir /tmp/basecamp-a\n```\n\nTo see which directories a scaffold profile uses:\n\n```bash\nlgs basecamp paths alice --json\n```\n\nAlso check that the module was captured at all. `lgs basecamp modules --show` prints the set that `install` and `launch` act on; a module missing from that table is never installed.\n\n## A QML type or singleton resolves to the wrong module\n\n**Symptom.** A QML singleton holds values that belong to a different module, a type renders as another module's component, or the log fills with `Invalid null URL` errors. The module works when run alone in the standalone app and breaks inside Basecamp, where several modules load together.\n\n**Cause.** QML composite types are cached for the whole process, keyed by the type name together with the URI of the module that declared it. A bare directory import such as `import \".\"` or `import \"./theme\"` declares no module, so that key carries an empty URI. When two modules loaded into the same host process each contain a file with the same base name, `Theme.qml` or `Card.qml` for instance, the cache can hand one module the other module's type.\n\n**Fix.** Keep your QML under a sub-directory and point `view` in `metadata.json` at it, for example `\"view\": \"qml/Main.qml\"`. When the view file sits at the project root, `logos-module-builder` 0.2.x and 0.3.x package that single file and nothing else, so no other QML file or directory reaches the package. Then make every QML directory you import a named module: give it a `qmldir` that declares a module name, and import it by that name.\n\n```\nqml/\n├── Main.qml\n└── SwapTheme/\n    ├── qmldir\n    └── Theme.qml\n```\n\n`qml/SwapTheme/qmldir`:\n\n```\nmodule SwapTheme\nsingleton Theme 1.0 Theme.qml\n```\n\n`qml/SwapTheme/Theme.qml` starts with `pragma Singleton`:\n\n```qml\npragma Singleton\nimport QtQuick\n\nQtObject {\n    readonly property color background: \"#101014\"\n}\n```\n\n`Main.qml` imports the module by name rather than by path:\n\n```qml\nimport QtQuick\nimport SwapTheme          // not: import \".\" or import \"SwapTheme\" as a path\n\nRectangle {\n    color: Theme.background\n}\n```\n\nTwo more points:\n\n- `logos-module-builder` generates a `qmldir` for the entry directory of your view, declaring `module com.logos.module.<name>`, and leaves a `qmldir` you ship yourself untouched. Subdirectories are yours to name.\n- A `qmldir` is a new file, so stage it before you build; see [A new file is missing from the built package](#a-new-file-is-missing-from-the-built-package).\n\n## Two instances collide\n\n**Symptom.** Two Basecamp windows show the same identity or the same message history, or one instance crashes shortly after the second starts.\n\n**Cause.** The two instances share a base directory, a runtime directory, or both. Shared state means a shared identity; a shared temp root means both instances try to bind the same module socket, and the second bind takes the socket from the first.\n\n**Fix.** Give each instance its own base directory and its own short runtime directory.\n\nWith scaffold, launch two different profiles from two terminals at the project root:\n\n```bash\n# Terminal 1\nlgs basecamp launch alice\n```\n\n```bash\n# Terminal 2\nlgs basecamp launch bob\n```\n\nBy hand:\n\n```bash\nTMPDIR=/tmp/bc-a XDG_RUNTIME_DIR=/tmp/bc-a LogosBasecamp --user-dir /tmp/basecamp-a &\nTMPDIR=/tmp/bc-b XDG_RUNTIME_DIR=/tmp/bc-b LogosBasecamp --user-dir /tmp/basecamp-b &\n```\n\nLaunching the same scaffold profile twice in parallel is not supported. If two instances still collide on a port after isolation, that is a module-level issue worth reporting against the module that owns the port.\n\n## Profiles share state on macOS\n\n**Symptom.** On macOS, two profiles that should be isolated show the same installed modules, the same identity, or the same history. `lgs basecamp paths <profile>` reports distinct directories, and the profile directories on disk really are separate.\n\n**Cause.** On macOS, Basecamp does not honour `XDG_DATA_HOME`, so the per-profile XDG isolation `launch` sets never reaches it. It locates its data tree through an environment override instead, and without one every profile collapses onto the shared `~/Library/Application Support/Logos/LogosBasecamp` (or `LogosBasecampDev` for a development build). This affects development builds as well as the portable bundle. Basecamp reads the override from `LOGOS_USER_DIR`, the environment-variable equivalent of `--user-dir`; the `LOGOS_DATA_DIR` that 0.1.x read is ignored.\n\n**Fix.** Let `launch` set it. `launch` always sets `LOGOS_USER_DIR` to an absolute per-profile path, on every host and stack. Confirm what a profile resolves with:\n\n```bash\nlgs basecamp paths alice --json\n```\n\nIf you declare `LOGOS_USER_DIR` yourself in `[basecamp.env]` or `[basecamp.profiles.<name>.env]`, your value wins, so make sure each profile gets its own, and use an absolute path when you start Basecamp yourself. A relative value can leave the UI unable to resolve its libraries: backend modules load, but the window never renders. Setting `LOGOS_DATA_DIR` has no effect.\n\n## Doctor reports drift\n\n**Symptom.** `lgs basecamp doctor` reports a warning about drift or about the pin set.\n\n**Cause and fix.** Doctor reports three kinds of drift, each with its own remedy:\n\n| Warning | Cause | Fix |\n|:---|:---|:---|\n| `basecamp drift: uncaptured` | A module source that `basecamp modules` would discover today is missing from `[modules]`. | Run `lgs basecamp modules`. It adds new sources and keeps existing entries. |\n| Dependency pin drift | A captured `role = \"dependency\"` entry points at a different revision from scaffold's default for that module. | Nothing, if the revision is intentional. Otherwise update the entry in `scaffold.toml`. |\n| Basecamp pin set | Exactly one of `[repos.basecamp]` and `[repos.lgpm]` is at scaffold's default, or Basecamp is still on a retired scaffold default such as 0.2.3. | For a retired default, run `lgs basecamp setup`, which moves both pins. Otherwise pin both to a matching pair, then run `setup`. See [Pinned versions](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#pinned-versions). |\n\nIf doctor reports that the Basecamp or `lgpm` binary is missing, or a command tells you Basecamp is not set up in this project, run `lgs basecamp setup`.\n\n## Collect diagnostics\n\nIf a problem survives all of the above, gather the evidence before reporting it:\n\n```bash\nlgs basecamp doctor --json\nlgs report --tail 500\n```\n\n`report` writes a `.tar.gz` bundle for a GitHub issue. It collects only a fixed list of logs and state, redacts what it collects, and lists anything it skipped. Inspect the archive with `tar -tzf <path>` before sharing it publicly, and file the issue with the template in [`logos-co/scaffold`](https://github.com/logos-co/scaffold/issues/new/choose).\n\nPer-run build logs live under `.scaffold/logs/`, one file per `install`. To watch a build as it happens instead:\n\n```bash\nlgs basecamp install --print-output\n```\n\n## Related documentation\n\n- [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md)\n- [Logos Scaffold Basecamp configuration reference](https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md)\n- [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md)\n- [Build and run a Logos core module](https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module.md)\n"},{"id":"build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference","title":"Logos Scaffold Basecamp configuration reference","summary":"Look up the `scaffold.toml` keys, launch environment, and directories behind `lgs basecamp`.","section":"build-an-app","doc_type":"reference","url":"https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference","markdown_url":"https://docs.logos.co/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md","permalink":"/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference","source":"https://github.com/logos-co/logos-docs/blob/main/docs/build-an-app/scaffold/reference/logos-scaffold-basecamp-configuration-reference.md","headings":[{"depth":2,"text":"`scaffold.toml` keys"},{"depth":3,"text":"`[repos.basecamp]` and `[repos.lgpm]`"},{"depth":3,"text":"`[modules.<name>]`"},{"depth":3,"text":"`[basecamp]`"},{"depth":3,"text":"`[basecamp.profiles.<name>]`"},{"depth":2,"text":"Launch environment"},{"depth":2,"text":"Runtime directory"},{"depth":2,"text":"Base directory"},{"depth":2,"text":"Related documentation"}],"markdown":"# Logos Scaffold Basecamp configuration reference\n\n#### Look up the `scaffold.toml` keys, launch environment, and directories behind `lgs basecamp`.\n\n> **Tip: Version**\n>\n> This document is accurate for `logos-scaffold` **0.4.0** built from `master` at commit `d4b15ff`, and Basecamp **0.3.0**.\n\nThis page lists what [Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md) reads and sets when it builds, installs, and launches Logos [modules](https://docs.logos.co/get-started/glossary.md#module) in [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp). For the workflow itself, see [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md). The full module-project contract is also available offline with `lgs basecamp docs`.\n\n## `scaffold.toml` keys\n\n### `[repos.basecamp]` and `[repos.lgpm]`\n\n`lgs basecamp setup` builds Basecamp and `lgpm` from these entries. Keep the two pins a matching pair; see [Pinned versions](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md#pinned-versions).\n\n| Key | Meaning |\n|:---|:---|\n| `source` | Git URL or flake reference of the repository. |\n| `pin` | Commit to build. |\n| `build` | Always `\"nix-flake\"`. |\n| `attr` | Flake output to build: `app` (development build) or a portable output such as `bin-appimage`, `bin-macos-app`, or `bin-bundle-dir` for Basecamp; `cli` for `lgpm`. |\n\n`attr` can also be a table keyed by Nix system, with the scalar form as the fallback for unlisted systems:\n\n```toml\n[repos.basecamp.attr]\naarch64-darwin = \"bin-macos-app\"\nx86_64-linux   = \"bin-appimage\"\n```\n\nWith a portable Basecamp output, `setup` builds the portable `lgpm`, and `install` and `launch` build each captured flake module's `lgx-portable` output instead of `lgx`, because a portable Basecamp loads only portable variants.\n\n### `[modules.<name>]`\n\nOne entry per module that `install` and `launch` act on. `lgs basecamp modules` writes these entries and never overwrites an existing one; you can also write them by hand.\n\n| Key | Meaning |\n|:---|:---|\n| `flake` | Flake reference of the module's `.lgx` output, for example `path:.#lgx`, `path:./ui#lgx`, or `github:<owner>/<repo>/<rev>#lgx`. A prebuilt [package](https://docs.logos.co/get-started/glossary.md#package) can be captured by path with `lgs basecamp modules --path <file.lgx>`. |\n| `role` | `\"project\"` for a module you build and ship, `\"dependency\"` for a runtime companion. `build` and `build-portable` skip dependencies. |\n| `standalone_app` | Optional flake app that `lgs basecamp run <name>` starts instead of the flake's default app. |\n\nThe `<name>` key must match the `name` in the module's `metadata.json`, because other modules refer to it by that name in their `dependencies`.\n\n### `[basecamp]`\n\n| Key | Meaning |\n|:---|:---|\n| `env` | Table of variables set on every profile's Basecamp process. |\n| `env_append` | Table of lists. Each list is `:`-joined and appended to the value `lgs` inherited, for path-style variables. |\n\n### `[basecamp.profiles.<name>]`\n\nAny profile name is accepted. Profiles other than `alice` and `bob` are seeded on first launch.\n\n| Key | Meaning |\n|:---|:---|\n| `env_file` | Dotenv-style `KEY=VALUE` file, relative to the project root or absolute. |\n| `env` | Table of variables for this profile only, written as a `[basecamp.profiles.<name>.env]` table. |\n| `runtime_dir` | Temporary and runtime directory for this profile. See [Runtime directory](#runtime-directory). |\n| `log_file` | File that every launch of this profile tees its output to. `launch --log-file` overrides it. |\n\n```toml\n[basecamp.profiles.maker]\nenv_file    = \".env\"\nruntime_dir = \"/tmp/lgs-maker\"\nlog_file    = \".scaffold/basecamp/profiles/maker/basecamp.log\"\n\n[basecamp.profiles.maker.env]\nSWAP_UI_AUTO_ROLE = \"maker\"\n```\n\n> **Warning**\n>\n> Write each `env` as its own `[… .env]` table, as above. `logos-scaffold` 0.4.0 silently ignores the inline form, `env = { KEY = \"value\" }`, for both `[basecamp]` and profiles.\n\n## Launch environment\n\n`lgs basecamp launch <profile>` adds these variables to the environment `lgs` itself inherited; it clears nothing. `<profile-dir>` is `.scaffold/basecamp/profiles/<profile>/`.\n\n| Variable | Value |\n|:---|:---|\n| `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_CACHE_HOME` | `<profile-dir>/xdg-config`, `xdg-data`, `xdg-cache` |\n| `TMPDIR`, `XDG_RUNTIME_DIR` | The profile's [runtime directory](#runtime-directory) |\n| `LOGOS_PROFILE` | The profile name |\n| `LOGOS_USER_DIR` | `<profile-dir>/xdg-data/Logos/LogosBasecamp`, with `Dev` appended for a development build. This is Basecamp's [base directory](#base-directory) for the profile. |\n\nYour own variables are layered on top in this order, last writer wins:\n\n1. `[basecamp.env_append]`\n1. The profile's `env_file`\n1. `[basecamp.env]`\n1. `[basecamp.profiles.<name>.env]`\n\n`LOGOS_USER_DIR` is post-processed after that layering: an absolute value you set is kept, a relative one is rewritten to absolute against the project root, and an empty one counts as unset and falls back to the profile default.\n\n`[basecamp.env_append]` cannot extend `QT_PLUGIN_PATH`, `QML2_IMPORT_PATH`, or `LD_LIBRARY_PATH` with Basecamp 0.3.0: its launcher script sets those three variables outright, replacing whatever it inherits.\n\nTo see what a profile resolves without launching it, run `lgs basecamp paths <profile> --json`.\n\n## Runtime directory\n\nEach module Basecamp loads opens a Unix domain socket under `TMPDIR`: `logos_<module>_<id>` in Basecamp 0.2.3 and later, `logos_token_<module>` before that. Scaffold gives every profile its own runtime directory and exports it as both `TMPDIR` and `XDG_RUNTIME_DIR`:\n\n1. `[basecamp.profiles.<name>].runtime_dir`, if set. A relative path is joined to the project root.\n1. Otherwise `/tmp/lgs-<project-hash>-<profile>`, where `<project-hash>` is the first eight hex characters of a SHA-256 of the project's path. The hash keeps two checkouts of the same project apart.\n\nThree constraints shape that default, and apply to any `runtime_dir` you set:\n\n- **Short.** The operating system caps a socket path at 104 bytes on macOS and 108 on Linux. A long runtime directory overflows the cap once the socket name is appended, and module loading aborts with `Unix socket path too long`.\n- **Outside the project.** A module flake is often the project root, and `nix build` refuses to copy the sockets a running Basecamp leaves behind, so a runtime directory inside the project breaks every build after the first launch.\n- **One per instance.** Two instances sharing a runtime directory can bind the same socket, and the second takes it from the first.\n\nScaffold creates the directory with mode `0700` before it touches the profile, and refuses one that is a symbolic link, is not a directory, or belongs to another user and cannot be restricted to `0700`.\n\n## Base directory\n\nBasecamp keeps its installed modules, UI plugins, per-module state, and logs in one base directory, with `modules/`, `plugins/`, `module_data/`, and `logs/` inside it. `logs/` holds one file per launch plus a `basecamp.log` link to the newest.\n\n| How Basecamp runs | Base directory |\n|:---|:---|\n| Through `lgs basecamp launch` | `<profile-dir>/xdg-data/Logos/LogosBasecamp`, with `Dev` appended for a development build |\n| `--user-dir <path>` (short form `-u`) | `<path>`, made absolute and created if it is missing |\n| `LOGOS_USER_DIR=<path>` | `<path>`, exactly as given, so use an absolute path |\n| Portable build with no override | `~/.local/share/Logos/LogosBasecamp` on Linux, `~/Library/Application Support/Logos/LogosBasecamp` on macOS |\n| Development build (`nix build .#app`) with no override | The same path with `Dev` appended, for example `~/.local/share/Logos/LogosBasecampDev` |\n\nOn macOS, Basecamp does not honour `XDG_DATA_HOME`, so without `LOGOS_USER_DIR` every instance of a build shares the one directory in `~/Library/Application Support`. `launch` always sets it. Basecamp 0.1.x's `LOGOS_DATA_DIR` is no longer read or set.\n\nModules that Basecamp bundles (`capability_module`, `modules_state`, `package_downloader`, `package_manager`, and `package_manager_ui`) load from next to its own binary, not from the base directory, so a `modules/` directory that lists only your own modules is expected.\n\n## Related documentation\n\n- [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md)\n- [Troubleshoot Logos module development with Basecamp](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md)\n- [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md)\n- [Scaffold command reference](https://github.com/logos-co/scaffold/blob/master/docs/commands.md) in the scaffold repository\n"},{"id":"get-started/introduction-to-logos","title":"What is Logos?","summary":"Learn how the Logos technology stack is organised as a modular operating system for decentralised applications.","section":"get-started","doc_type":"concept","url":"https://docs.logos.co/get-started/what-is-logos","markdown_url":"https://docs.logos.co/get-started/what-is-logos.md","permalink":"/get-started/what-is-logos","source":"https://github.com/logos-co/logos-docs/blob/main/docs/get-started/introduction-to-logos.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Architecture"},{"depth":3,"text":"The runtime"},{"depth":3,"text":"The networking layer"},{"depth":3,"text":"Modules"},{"depth":3,"text":"Apps"}],"markdown":"# What is Logos?\n\n#### Learn how the Logos technology stack is organised as a modular operating system for decentralised applications.\n\n**Logos is a toolbox for building decentralised applications.** As a modular, microkernel-based technology stack, its structure can be compared to the Linux operating system, which consists of a kernel, a networking stack, a set of system services, and the applications that run on top of them. Different distributions share the same kernel but assemble different services and applications on top of it.\n\nLogos follows the same structure. At its foundation sits the **Logos runtime**, a microkernel-style core that provides the primitives a decentralised application requires. Above the runtime sits a **networking layer**, which provides [peer discovery](https://docs.logos.co/get-started/glossary.md#peer-discovery), connection management, and privacy-preserving communication through a [mixnet](https://docs.logos.co/get-started/glossary.md#mixnet). Above that sit the **[modules](https://docs.logos.co/get-started/glossary.md#module)** - pluggable components that provide a specific capability, such as storage, messaging, or blockchain consensus. At the top of the stack sit the **[apps](https://docs.logos.co/get-started/glossary.md#app)** that compose these modules into usable products.\n\nLogos ships with a default configuration consisting of the runtime plus storage, messaging, and blockchain modules. This configuration is not fixed: developers can assemble other distributions from a different selection of modules.\n\n## The basics\n\n- Logos is organised into four layers: a runtime at the base, a privacy-preserving networking layer above it, a set of pluggable modules that provide specific capabilities, and applications on top that are composed of those modules.\n- [Logos Basecamp](https://docs.logos.co/basecamp/install-logos-basecamp.md) is a local-first launcher for peer-to-peer apps built on Logos, and the default entry point to the stack. It runs the Logos runtime locally, loads a configured module profile, and provides a unified interface. The **Logos Node** runs the same runtime without a user interface, for validators, operators, or backend services.\n- An application built on Logos inherits metadata protection and support for private state from the infrastructure layer regardless of whether its developer has implemented privacy-preserving measures directly.\n\n## Architecture\n\nLogos is organised into distinct layers, each with a defined responsibility: the runtime, the networking layer, modules, and apps.\n\n![The Logos architecture.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/get-started/assets/introduction-to-logos/logos-tech-diagram.png)\n\n### The runtime\n\nThe **Logos runtime** follows a microkernel design: it performs a minimal set of functions and delegates all other responsibilities to modules that run on top of it. This differs from a monolithic kernel such as Linux, where the networking stack and many other services run inside the kernel itself. The runtime's responsibilities are limited to loading and unloading modules, managing each module's lifecycle, and providing a mechanism for modules to locate and call one another.\n\nThe runtime is implemented in a C++ library called [liblogos](https://github.com/logos-co/logos-liblogos). Three supporting modules operate alongside it. The **Process Manager Module** runs each loaded module in its own operating system process. If a module fails, the Process Manager Module records the failure and restarts the module using an exponential backoff, so that a fault in one module does not affect the rest of the system. The **Developer Module** provides visibility into loaded modules, their registered objects, and their interactions, together with tracing, hot reload, and live debugging through a JSON-RPC interface. The **Package Manager Module** retrieves modules, currently from GitHub and in future from peer-to-peer storage, verifies them against cryptographic hashes, resolves dependencies, and applies incremental updates rather than re-downloading a module in full each time it changes.\n\nThis separation allows individual modules to be developed, tested, and updated independently of the runtime and of one another.\n\n### The networking layer\n\nThe networking layer sits above the runtime and is represented in the stack as discovery, peering, and mixnet. It determines how Logos nodes locate one another, establish connections, and exchange messages. Unlike a conventional networking stack, this layer is designed to protect the privacy of communications between participants, as a property of the network rather than of any individual application.\n\nThis layer comprises several components. **Service Discovery** allows a node to locate other nodes that offer a required capability without consulting a central directory. It adapts the DISC-NG protocol for use with libp2p's Kademlia-DHT: each node publishes a signed record describing its address and capabilities, and other nodes query for records that match the capabilities they require without participating in the full distributed hash table.\n\n**The libp2p mixnet** is the privacy-preserving transport layer, based on the design principles used in anonymity networks such as Tor and Nym. Messages are routed through a sequence of intermediary relay nodes that shuffle and delay traffic before forwarding it, preventing an observer from correlating senders with receivers using timing or volume analysis. The protocol supports both one-way messages and request-response interactions, and includes cover traffic to maintain a sufficient set of plausible senders.\n\n**RLN and Zerokit** add spam protection to the network. A Rate Limiting Nullifier allows each network participant to send messages up to a defined rate while remaining anonymous. If a participant exceeds that rate, the protocol reveals their identity, allowing them to be slashed or removed. This cryptography is implemented in [Zerokit](https://github.com/vacp2p/zerokit) and is used both for denial-of-service protection on the mixnet and for rate limiting within Logos Delivery.\n\n**De-MLS** extends private messaging from one-to-one conversations to group conversations. It is a decentralised implementation of Messaging Layer Security in which a group is managed by multiple stewards rather than a single administrator.\n\nThe networking layer is itself packaged as a module and loaded by the runtime in the same way as storage or messaging. Once loaded, it provides the private transport used by every other module to communicate across the network, independent of whether that traffic consists of file storage requests, chat messages, or transactions.\n\n### Modules\n\nModules are self-contained components that sit above the networking layer, each providing a defined capability. Logos includes three foundational modules, and the architecture supports the addition of further modules.\n\nThe [**Blockchain**](https://docs.logos.co/blockchain.md) module provides decentralised compute and consensus. It is built on **[Cryptarchia](https://docs.logos.co/get-started/glossary.md#cryptarchia)**, a private proof-of-stake consensus mechanism that makes it computationally infeasible for block proposers to be linked to their proposals, with stronger privacy provided by the **[Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network)**. The **Logos Execution Zone (LEZ)**, a Layer 2 execution environment, supports [program](https://docs.logos.co/get-started/glossary.md#program) deployment with both public and private [account](https://docs.logos.co/get-started/glossary.md#account) states.\n\nThe [**Messaging**](https://docs.logos.co/messaging.md) module provides private, censorship-resistant communication between parties and consists of two components. [Logos Delivery](https://github.com/logos-messaging/logos-delivery) provides the [transport](https://docs.logos.co/get-started/glossary.md#transport) layer, implementing publish-subscribe messaging for reliable delivery. [Logos Chat](https://github.com/logos-messaging/logos-chat) uses Delivery as its transport and provides encrypted one-to-one conversations, implemented with double-ratchet encryption, with support for group conversations under development using de-MLS.\n\nThe [**Storage**](https://docs.logos.co/storage.md) module provides decentralised file storage and retrieval using content-addressed data. It exposes an API for storing a file and receiving a content identifier ([CID](https://docs.logos.co/get-started/glossary.md#cid)) in return, and for retrieving a file given its CID.\n\n**User modules** extend the architecture beyond the foundational set. Any developer can build a module that integrates with the same infrastructure. The runtime loads user modules, manages their lifecycle, and enables them to communicate with other modules, whether those modules are provided by Logos or by third parties.\n\n### Apps\n\nApps are the decentralised applications that compose the modules described above. A chat application could use the messaging and storage modules; a decentralised finance application could use the blockchain module and the LEZ; a file-sharing application could use the storage module.\n\n[Logos Basecamp](https://docs.logos.co/basecamp/install-logos-basecamp.md) is a local-first launcher for peer-to-peer apps built on Logos, and the default entry point to the stack. It starts the runtime, loads the configured module profile, and provides a unified interface. By default, it includes a set of applications for each foundational module: a wallet for managing tokens, a chat interface for encrypted messaging, a file-sharing tool, and an explorer for inspecting blockchain and LEZ activity. The [Logos Node](https://docs.logos.co/run-a-node.md) provides an alternative entry point, starting the same runtime without a user interface. Because a distribution is defined by its selected modules rather than by its launcher, developers can assemble distributions for purposes other than the default configuration.\n"},{"id":"get-started/coding-with-agents","title":"Use the Logos docs with coding agents","summary":"Give Claude Code, Cursor, Codex, or another coding agent the Logos documentation so it works from current docs instead of memory.","section":"get-started","doc_type":"procedure","url":"https://docs.logos.co/get-started/coding-with-agents","markdown_url":"https://docs.logos.co/get-started/coding-with-agents.md","permalink":"/get-started/coding-with-agents","source":"https://github.com/logos-co/logos-docs/blob/main/docs/get-started/coding-with-agents.md","headings":[{"depth":2,"text":"Read the docs as Markdown"},{"depth":2,"text":"Add the Logos docs to `AGENTS.md`"},{"depth":2,"text":"Install the Logos docs skill"},{"depth":2,"text":"Report a problem"}],"markdown":"# Use the Logos docs with coding agents\n\n#### Give Claude Code, Cursor, Codex, or another coding agent the Logos documentation so it works from current docs instead of memory.\n\nLogos changes with every Testnet release, and most of it is newer than the training data of any coding agent. An agent that answers from memory invents `metadata.json` fields, Nix flake outputs, and `logosctl` flags. This page shows three ways to give an agent the current docs, from the lightest to the most thorough:\n\n| Option | What the agent gets | Set-up time |\n| --- | --- | --- |\n| [Markdown and `llms.txt`](#read-the-docs-as-markdown) | Every page as plain Markdown, plus an index | None |\n| [`AGENTS.md` block](#add-the-logos-docs-to-agentsmd) | A compact index of the docs in every prompt | One command |\n| [Agent skill](#install-the-logos-docs-skill) | Instructions the agent loads when it works on Logos code | One command |\n\n## Read the docs as Markdown\n\nAgents read Markdown far more efficiently than HTML. Every page on this site has a Markdown version:\n\n- **Any page:** add `.md` to its URL, for example [`/get-started/what-is-logos.md`](https://docs.logos.co/get-started/what-is-logos.md).\n- **Index of every page**, each with a one-line summary: [`/llms.txt`](https://docs.logos.co/llms.txt), also at `/.well-known/llms.txt`.\n- **One section:** `/<section>/llms.txt` for the index and `/<section>/llms-full.txt` for the full text, for example [`/core/llms.txt`](https://docs.logos.co/core/llms.txt).\n- **Everything in one file:** [`/llms-full.txt`](https://docs.logos.co/llms-full.txt), about 1 MB.\n- **Structured index** with each section's use cases, for tools: [`/agents/docs.json`](https://docs.logos.co/agents/docs.json).\n\nEach page also has a **Copy content** button that copies the page as Markdown, and an **Open in Claude** button that starts a chat about the page. Its arrow menu switches to ChatGPT, which the button then remembers, and shows the Markdown version.\n\n> **Tip**\n>\n> To point an agent at the docs without any set-up, start your prompt with: \"Read https://docs.logos.co/llms.txt and use the Logos docs for this task.\"\n\n## Add the Logos docs to `AGENTS.md`\n\n`AGENTS.md` (or `CLAUDE.md` for Claude Code) is loaded into every prompt, so the agent does not have to decide to look anything up. The block below tells the agent to prefer the docs over memory and lists every page with a short summary, in about 12 KB.\n\n1. From the root of your project, append the block to `AGENTS.md`:\n\n   ```bash\n   curl -fsSL https://docs.logos.co/agents/AGENTS.md >> AGENTS.md\n   ```\n\n1. If you use Claude Code, make sure `CLAUDE.md` includes it. Either append the block to `CLAUDE.md` instead, or add a line containing `@AGENTS.md` to `CLAUDE.md`.\n\nThe block is generated from the docs on every site build. To refresh it, replace the text between `<!-- BEGIN logos-docs` and `<!-- END logos-docs -->` with a new download.\n\n## Install the Logos docs skill\n\nA skill is a set of instructions that an agent loads only when the task needs it. The Logos docs skill tells the agent where the docs are, to check the version banner on each page, and to check every `metadata.json` it writes against the docs.\n\n- **Claude Code**, for one project:\n\n  ```bash\n  mkdir -p .claude/skills/logos-docs\n  curl -fsSL https://docs.logos.co/agents/skills/logos-docs/SKILL.md \\\n    -o .claude/skills/logos-docs/SKILL.md\n  ```\n\n  To install it for all your projects, use `~/.claude/skills/logos-docs` instead.\n\n- **Other agents** that support skills: download [`SKILL.md`](https://docs.logos.co/agents/skills/logos-docs/SKILL.md) into the agent's skills folder.\n\n## Report a problem\n\nIf an agent gets something wrong because a page is wrong, unclear, or missing, select **Report issue** above the page title. It opens a GitHub issue with the page already filled in.\n"},{"id":"get-started/logos-ecosystem-repositories","title":"Logos ecosystem repositories","summary":"Review the key repositories across the logos organisations.","section":"get-started","doc_type":"reference","url":"https://docs.logos.co/get-started/logos-ecosystem-repositories","markdown_url":"https://docs.logos.co/get-started/logos-ecosystem-repositories.md","permalink":"/get-started/logos-ecosystem-repositories","source":"https://github.com/logos-co/logos-docs/blob/main/docs/get-started/logos-ecosystem-repositories.md","headings":[{"depth":2,"text":"Orientation and contribution resources"},{"depth":2,"text":"Logos Basecamp"},{"depth":2,"text":"Logos Core"},{"depth":2,"text":"Core modules and applications"},{"depth":2,"text":"Logos Blockchain"},{"depth":2,"text":"Logos Execution Zone"},{"depth":2,"text":"Logos messaging"},{"depth":2,"text":"Logos Storage"},{"depth":2,"text":"AnonComms"},{"depth":2,"text":"Standards, governance, and verification"}],"markdown":"# Logos ecosystem repositories\n\n#### Review the key repositories across the logos organisations.\n \nThe Logos project is organised into several GitHub organisations, each focused on one part of the system. This page highlights the most important repositories for understanding the project and getting started with development. The selection is curated rather than exhaustive.\n \n## Orientation and contribution resources\n \nThese repositories explain what Logos is building and how to take part, with no code to run.\n \n| Repository | Description |\n|:---|:---|\n| [logos-docs](https://github.com/logos-co/logos-docs) | Documentation, standards, and templates for Logos projects. |\n| [logos-web](https://github.com/logos-co/logos-web) | Monorepo for logos.co: the website, Field Guide, Research, Payload CMS, and browser demos. |\n| [logos-workspace](https://github.com/logos-co/logos-workspace) | Workspace CLI for developing across interdependent Logos repositories. |\n| [roadmap](https://github.com/logos-co/roadmap) | A Markdown-based view of the Logos roadmap. |\n| [rfp](https://github.com/logos-co/rfp) | Grants for building in the Logos ecosystem. |\n \n## Logos Basecamp\n\nLogos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) is a local-first launcher for peer-to-peer apps built on Logos.\n\n| Repository | Description |\n|:---|:---|\n| [logos-basecamp](https://github.com/logos-co/logos-basecamp) | The Logos Basecamp repository. |\n\n## Logos Core\n \nLogos Core is the modular runtime that ties applications together.\n\n| Repository | Description |\n|:---|:---|\n| [scaffold](https://github.com/logos-co/scaffold) | CLI that bootstraps a runnable Logos application environment: LEZ program projects and the Basecamp module loop. Ships the `logos-scaffold` and `lgs` binaries. See [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md). |\n| [logos-liblogos](https://github.com/logos-co/logos-liblogos) | The core runtime library for the Logos modular application platform. |\n| [logos-logoscore-cli](https://github.com/logos-co/logos-logoscore-cli) | The headless CLI runtime for the Logos modular application platform. |\n| [logos-logoscore-py](https://github.com/logos-co/logos-logoscore-py) | Python wrapper for the [logoscore](https://docs.logos.co/get-started/glossary.md#logoscore) CLI. |\n| [logos-package-manager](https://github.com/logos-co/logos-package-manager) | C++ library and CLI for local Logos [package](https://docs.logos.co/get-started/glossary.md#package) management. |\n| [logos-package-downloader](https://github.com/logos-co/logos-package-downloader) | [Module](https://docs.logos.co/get-started/glossary.md#module) download tooling. No official description; purpose inferred. |\n| [logos-module-builder](https://github.com/logos-co/logos-module-builder) | Nix and CMake scaffolding library for building Logos [modules](https://docs.logos.co/get-started/glossary.md#module): project templates, the `logos_module()` CMake helper, and the `mkLogosModule` Nix builder. |\n| [logos-app-builder](https://github.com/logos-co/logos-app-builder) | Shared Nix library for building Logos UI applications. |\n| [Overwatch](https://github.com/logos-co/Overwatch) | Self-contained services application framework. |\n \n## Core modules and applications\n \nModules plug into Logos Core to provide specific capabilities, and user-interface repositories are the front-end applications built on those modules. Modules and applications live in both organisations.\n \n| Repository | Description |\n|:---|:---|\n| [logos-chat-module](https://github.com/logos-co/logos-chat-module) | A Logos [Core module](https://docs.logos.co/get-started/glossary.md#core-module) that exposes [Logos Chat](https://docs.logos.co/get-started/glossary.md#logos-chat). |\n| [logos-chat-ui](https://github.com/logos-co/logos-chat-ui) | UI application for the Logos Chat SDK proof of concept. |\n| [logos-wallet-module](https://github.com/logos-co/logos-wallet-module) | Early stage wallet module built on go-wallet-sdk. |\n| [logos-blockchain-module](https://github.com/logos-blockchain/logos-blockchain-module) | Core module wrapping logos-blockchain-c. |\n| [logos-blockchain-ui](https://github.com/logos-blockchain/logos-blockchain-ui) | Blockchain UI application for Logos Core. |\n| [logos-execution-zone-module](https://github.com/logos-blockchain/logos-execution-zone-module) | [Logos Execution Zone](https://docs.logos.co/get-started/glossary.md#logos-execution-zone) module for Logos Core. No official description; purpose inferred. |\n| [logos-execution-zone-wallet-ui](https://github.com/logos-blockchain/logos-execution-zone-wallet-ui) | Logos Execution Zone wallet UI. No official description; purpose inferred. |\n| [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module) | A Logos module that integrates libp2p networking capabilities. |\n \n## Logos Blockchain\n \nThe [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) is the foundational infrastructure of the Logos technology stack.\n \n| Repository | Description |\n|:---|:---|\n| [logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) | The Logos blockchain repository. |\n| [logos-blockchain-block-explorer-template](https://github.com/logos-blockchain/logos-blockchain-block-explorer-template) | Python block explorer and indexer template. |\n| [nimbos](https://github.com/logos-co/nimbos) | Nim client for the Logos chain. |\n \n## Logos Execution Zone\n \nThe Logos Execution Zone ([LEZ](https://docs.logos.co/get-started/glossary.md#lez)) is the primary execution layer for applications built on the Logos stack.\n \n| Repository | Description |\n|:---|:---|\n| [logos-execution-zone](https://github.com/logos-blockchain/logos-execution-zone) | The LEZ  repository. |\n| [lez-programs](https://github.com/logos-blockchain/lez-programs) | Essential Logos Execution Zone [programs](https://docs.logos.co/get-started/glossary.md#program) built by Logos. |\n| [lez-multisig](https://github.com/logos-co/lez-multisig) | Multisig built with the lez-framework program macro. |\n| [logos-lez-rln](https://github.com/logos-co/logos-lez-rln) | Logos Execution Zone program for RLN registration. |\n| [logos-sql-zone](https://github.com/logos-blockchain/logos-sql-zone) | Password-manager demo built with the [Zone SDK](https://docs.logos.co/get-started/glossary.md#zone-sdk). |\n| [lez-fuzzing](https://github.com/logos-blockchain/lez-fuzzing) | Adversarial testing and fuzzing infrastructure for LEZ. |\n \n## Logos messaging\n\n[Logos Messaging](https://docs.logos.co/get-started/glossary.md#logos-messaging) is the project's decentralised communication system.\n\n| Repository | Description |\n|:---|:---|\n| [logos-delivery](https://github.com/logos-messaging/logos-delivery) | Logos messaging protocols implemented in Nim, formerly nwaku. |\n| [logos-delivery-js](https://github.com/logos-messaging/logos-delivery-js) | Logos messaging protocols implemented in JavaScript and TypeScript, formerly [Waku](https://docs.logos.co/get-started/glossary.md#waku) v2. |\n| [logos-delivery-python-bindings](https://github.com/logos-messaging/logos-delivery-python-bindings) | Python wrapper around the node. |\n| [sds-go-bindings](https://github.com/logos-messaging/sds-go-bindings) | Go bindings for the SDS reliability protocol. No official description; purpose inferred. |\n| [nim-sds](https://github.com/logos-messaging/nim-sds) | Nim implementation of the end-to-end reliability protocol. |\n| [nim-segmentation](https://github.com/logos-messaging/nim-segmentation) | Message segmentation. No official description; purpose inferred. |\n| [logos-delivery-rlnv2-contract](https://github.com/logos-messaging/logos-delivery-rlnv2-contract) | RLN-v2 smart contracts, written in Solidity. |\n| [logos-lips](https://github.com/logos-co/logos-lips) | Protocol specifications. |\n| [logos-delivery-simulator](https://github.com/logos-messaging/logos-delivery-simulator) | Simulates a network with multiple nodes, traffic, and users. |\n| [logos-delivery-interop-tests](https://github.com/logos-messaging/logos-delivery-interop-tests) | Interoperability tests. No official description; purpose inferred. |\n| [logos-chat](https://github.com/logos-messaging/logos-chat) | Supporting library for Logos Chat, written in Rust. |\n| [awesome-logos-delivery](https://github.com/logos-messaging/awesome-logos-delivery) | Community-curated list of resources. |\n\n## Logos Storage\n\n[Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) is the project's privacy-preserving, decentralised storage system.\n\n| Repository | Description |\n|:---|:---|\n| [logos-storage-nim](https://github.com/logos-storage/logos-storage-nim) | The storage node and durability engine, formerly nim-[codex](https://docs.logos.co/get-started/glossary.md#codex). |\n| [logos-storage-nim-dht](https://github.com/logos-storage/logos-storage-nim-dht) | A DHT based on Discv5 with libp2p provider-record support. |\n| [circom-compat-ffi](https://github.com/logos-storage/circom-compat-ffi) | FFI for circom-compat (ark-circom), used for zero-knowledge proving. |\n| [logos-storage-research](https://github.com/logos-storage/logos-storage-research) | Durability-engine research. |\n| [logos-storage-nim-cs-dist-tests](https://github.com/logos-storage/logos-storage-nim-cs-dist-tests) | Distributed system tests for the node. |\n| [logos-storage-local-harness](https://github.com/logos-storage/logos-storage-local-harness) | A bash harness for running the node without containers. |\n| [transport-over-mix](https://github.com/logos-storage/transport-over-mix) | Transport-layer abstraction over [Mix](https://docs.logos.co/get-started/glossary.md#mix), with spec and reference implementation. |\n| [libp2p-mix-transport](https://github.com/logos-storage/libp2p-mix-transport) | Experiments with a storage transport over the libp2p Mix protocol. |\n| [mix-hidden-services](https://github.com/logos-storage/mix-hidden-services) | Hidden services over Mix. |\n| [lioness_blockcipher](https://github.com/logos-storage/lioness_blockcipher) | Rust implementation of the LIONESS wide-block cipher. |\n| [logos-storage-docs-obsidian](https://github.com/logos-storage/logos-storage-docs-obsidian) | Indexed, searchable documentation as an Obsidian vault. |\n\n## AnonComms\n\nAnonComms focuses on building the core elements that allow privacy-preserving communication and coordination across the Logos stack. The work is at early stages, see the latest update in the [roadmap](https://roadmap.logos.co/anoncomms/).\n\n## Standards, governance, and verification\n \nThese repositories define how Logos documents, proposes, and verifies changes.\n \n| Repository | Description |\n|:---|:---|\n| [logos-lips](https://github.com/logos-co/logos-lips) | Logos Improvement Proposals Index.|\n| [logos-doctest](https://github.com/logos-co/logos-doctest) | Executable documentation that can be verified and rendered. |\n| [logos-tutorial](https://github.com/logos-co/logos-tutorial) | Tutorial materials |\n"},{"id":"get-started/glossary","title":"Glossary","summary":"A list of some common terms and phrases specific to the Logos ecosystem.","section":"get-started","doc_type":null,"url":"https://docs.logos.co/get-started/glossary","markdown_url":"https://docs.logos.co/get-started/glossary.md","permalink":"/get-started/glossary","source":"https://github.com/logos-co/logos-docs/blob/main/docs/get-started/glossary.md","headings":[{"depth":2,"text":"A"},{"depth":3,"text":"Account"},{"depth":3,"text":"App"},{"depth":2,"text":"B"},{"depth":3,"text":"Basecamp"},{"depth":3,"text":"Bedrock"},{"depth":3,"text":"Blend Network"},{"depth":3,"text":"Blend node"},{"depth":3,"text":"Blend Protocol"},{"depth":3,"text":"Bootstrapping"},{"depth":2,"text":"C"},{"depth":3,"text":"Catalogue"},{"depth":3,"text":"Channel"},{"depth":3,"text":"Channel note"},{"depth":3,"text":"CID"},{"depth":3,"text":"Codex"},{"depth":3,"text":"Content topic"},{"depth":3,"text":"Core module"},{"depth":3,"text":"Core node"},{"depth":3,"text":"Cover traffic"},{"depth":3,"text":"Cryptarchia"},{"depth":2,"text":"D"},{"depth":3,"text":"Delivery module"},{"depth":3,"text":"DNS discovery"},{"depth":2,"text":"E"},{"depth":3,"text":"EmPoWering"},{"depth":3,"text":"ENR"},{"depth":3,"text":"Epoch"},{"depth":3,"text":"Ethereum Node Record"},{"depth":2,"text":"F"},{"depth":3,"text":"Filter"},{"depth":2,"text":"G"},{"depth":3,"text":"GMS"},{"depth":3,"text":"GossipSub"},{"depth":3,"text":"Group Master Secret"},{"depth":2,"text":"I"},{"depth":3,"text":"Inscription"},{"depth":2,"text":"L"},{"depth":3,"text":"LEE"},{"depth":3,"text":"LEZ"},{"depth":3,"text":"`lgpm`"},{"depth":3,"text":"`lgpd`"},{"depth":3,"text":"`.lgx`"},{"depth":3,"text":"Light node"},{"depth":3,"text":"Light push"},{"depth":3,"text":"LIP"},{"depth":3,"text":"`lm`"},{"depth":3,"text":"Locator"},{"depth":3,"text":"Logos Blockchain"},{"depth":3,"text":"Logos Chat"},{"depth":3,"text":"Logos Delivery"},{"depth":3,"text":"Logos Execution Environment"},{"depth":3,"text":"Logos Execution Zone"},{"depth":3,"text":"Logos Messaging"},{"depth":3,"text":"Logos node"},{"depth":3,"text":"`logos-module-builder`"},{"depth":3,"text":"`logos-module-viewer`"},{"depth":3,"text":"`logos-standalone-app`"},{"depth":3,"text":"Logos Storage"},{"depth":3,"text":"`logoscore`"},{"depth":3,"text":"LSSA"},{"depth":2,"text":"M"},{"depth":3,"text":"Mantle"},{"depth":3,"text":"Message"},{"depth":3,"text":"Mining"},{"depth":3,"text":"Mix"},{"depth":3,"text":"Mixnet"},{"depth":3,"text":"Module"},{"depth":3,"text":"Mostly offline"},{"depth":2,"text":"N"},{"depth":3,"text":"Nescience"},{"depth":3,"text":"Nomos"},{"depth":3,"text":"Node key"},{"depth":3,"text":"Note"},{"depth":3,"text":"NPK"},{"depth":3,"text":"NSSA"},{"depth":3,"text":"NSK"},{"depth":3,"text":"Nullifier keys"},{"depth":3,"text":"Nullifier Public Key"},{"depth":3,"text":"Nullifier Secret Key"},{"depth":2,"text":"O"},{"depth":3,"text":"Out-of-band"},{"depth":2,"text":"P"},{"depth":3,"text":"Package"},{"depth":3,"text":"PDA"},{"depth":3,"text":"Peer discovery"},{"depth":3,"text":"Peer exchange"},{"depth":3,"text":"Peer ID"},{"depth":3,"text":"PoL"},{"depth":3,"text":"PPoS"},{"depth":3,"text":"Private account"},{"depth":3,"text":"Private Proof of Stake"},{"depth":3,"text":"Program"},{"depth":3,"text":"Program Derived Address"},{"depth":3,"text":"Proof of Leadership"},{"depth":3,"text":"Proposer privacy"},{"depth":3,"text":"Public account"},{"depth":2,"text":"R"},{"depth":3,"text":"Relay"},{"depth":2,"text":"S"},{"depth":3,"text":"SDP"},{"depth":3,"text":"Sealing keys"},{"depth":3,"text":"Service"},{"depth":3,"text":"Service Declaration Protocol"},{"depth":3,"text":"Service Reward Distribution Protocol"},{"depth":3,"text":"Slot"},{"depth":3,"text":"SRDP"},{"depth":3,"text":"Storage module"},{"depth":3,"text":"Store"},{"depth":2,"text":"T"},{"depth":3,"text":"Token definition account"},{"depth":3,"text":"Token holding account"},{"depth":3,"text":"Token program"},{"depth":3,"text":"Transport"},{"depth":2,"text":"U"},{"depth":3,"text":"UI Module"},{"depth":2,"text":"V"},{"depth":3,"text":"Viewing keys"},{"depth":3,"text":"Viewing Public Key"},{"depth":3,"text":"Viewing Secret Key"},{"depth":3,"text":"VPK"},{"depth":3,"text":"VSK"},{"depth":2,"text":"W"},{"depth":3,"text":"Waku"},{"depth":2,"text":"Z"},{"depth":3,"text":"Zone SDK"},{"depth":3,"text":"Zone"}],"markdown":"# Glossary\n\nA list of some common terms and phrases specific to the Logos ecosystem.\n\n---\n\n## A\n\n### Account\n\nThe basic organisational unit of the LEZ state, with all persistent data stored in accounts. Accounts can be either public or private, and can also represent LEZ programs. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### App\n\nIn the context of Logos modules, a user-facing bundle of a UI package combined with its core dependencies. See [Package](#package).\n\n---\n\n## B\n\n### Basecamp\n\nA local-first launcher for peer-to-peer apps built on Logos, where users install and run Logos modules and apps.\n\n### Bedrock\n\nThe foundational layer of the Logos Blockchain, operating as a decentralised peer-to-peer network. See [About Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md).\n\n### Blend Network\n\nA Service that improves privacy protections for block proposers on the Logos Blockchain. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md).\n\n### Blend node\n\nA Logos Blockchain node that participates in the Blend Network. Also known as a **core node**. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md).\n\n### Blend Protocol\n\nThe protocol underlying the Blend Network, which improves privacy protections for Logos Blockchain block proposers. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md).\n\n### Bootstrapping\n\nThe process a Logos Blockchain or Delivery node undergoes when first connected to the network. For a Logos Blockchain node, this involves \"catching up\" on the history of the blockchain.\n\n---\n\n## C\n\n### Catalogue\n\nA repository that lists the [packages](#package) available to install in Basecamp, identified by a `logos-repo.json` file. Basecamp ships with the official Logos catalogue configured and merges in any others you add.\n\n### Channel\n\nA lightweight virtual chain of ordered message logs that represent the state of a Zone on Bedrock. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md#logos-channels).\n\n### Channel note\n\nA note locked by a channel, representing the value bridged into that channel's Zone. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md#token-bridging).\n\n### CID\n\n**Content Identifier**, used to identify files for the storage module.\n\n### Codex\n\nA preceding protocol that informed the development of Logos Storage.\n\n### Content topic\n\nA string attached to messages sent via Logos Delivery to enable protocol-level features like selective message processing and retrieval based on specific criteria. See [About content topics](https://docs.logos.co/messaging/concepts/content-topics.md).\n\n### Core module\n\nA module that does not include a user interface view and provides backend functionality.\n\n### Core node\n\nA Logos Blockchain node that chooses to participate in the Blend Network. Also known as a **Blend node**. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md).\n\n### Cover traffic\n\nDummy messages sent by Blend nodes that, together with the other components of the Blend Protocol, improves proposer privacy guarantees. See [Cover traffic](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#cover-traffic).\n\n### Cryptarchia\n\nThe Private Proof of Stake consensus protocol used by the Logos Blockchain. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md).\n\n---\n\n## D\n\n### Delivery module\n\nA module that uses the Logos Delivery network to send messages and subscribe to content topics.\n\n### DNS discovery\n\nA peer discovery mechanism used in Logos Delivery that allows the retrieval of an Ethereum Node Record (ENR) tree from the TXT field of a domain name, enabling the storage of node connection details and promoting decentralisation. See [About DNS discovery](https://docs.logos.co/messaging/concepts/understand-peer-discovery/dns-discovery.md).\n\n---\n\n## E\n\n### EmPoWering\n\nA term used to describe how Proof of Work (PoW) is used on the Logos Blockchain, both to mine notes and to send Mantle transactions through the Blend Network. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md#mining-notes) and [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#sending-mantle-transactions-via-proof-of-work).\n\n### ENR\n\n**Ethereum Node Record** - a specification used in Logos Delivery to represent and identify nodes, facilitating discovery and communication within the network. Besides connection details, ENR also includes node configuration information like enabled protocol and shards.\n\n### Epoch\n\nA time unit used by the Logos Blockchain, about 7.5 days long. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md#time-units).\n\n### Ethereum Node Record\n\nA specification used in Logos Delivery to represent and identify nodes, facilitating discovery and communication within the network. Also known as **ENR**. Besides connection details, ENR also includes node configuration information like enabled protocol and shards.\n\n---\n\n## F\n\n### Filter\n\nA protocol that enables Logos Delivery light nodes to selectively subscribe to specific messages transmitted by peers using content topics. It is designed to be a lightweight alternative for accessing the Relay network. See [Understand Logos Delivery protocols](https://docs.logos.co/messaging/concepts/protocols.md#filter).\n\n---\n\n## G\n\n### GMS\n\nA **Group Master Secret** - a 32-byte secret used on the LEZ that, when sealed with another party's public sealing key, allows them to create a local instance of a shared LEZ account. See [Set up a shared private LEZ account](https://docs.logos.co/lez/accounts/set-up-shared-private-lez-account.md).\n\n### GossipSub\n\nA protocol for efficient and scalable information dissemination in decentralised networks commonly used in blockchain systems. See [About network domains](https://docs.logos.co/messaging/concepts/network-domains.md#gossip-domain).\n\n### Group Master Secret\n\nA 32-byte secret used on the LEZ that, when sealed with another party's public sealing key, allows them to create a local instance of a shared LEZ account. Also known as a **GMS**. See [Set up a shared private LEZ account](https://docs.logos.co/lez/accounts/set-up-shared-private-lez-account.md).\n\n---\n\n## I\n\n### Inscription\n\nA type of Mantle operation that writes data to the Logos Blockchain. Inscriptions are often used to represent Zone state updates. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md).\n\n---\n\n## L\n\n### LEE\n\nThe **Logos Execution Environment** - the virtual machine that runs on the Logos Execution Zone. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### LEZ\n\nThe **Logos Execution Zone** - the primary execution layer for general-purpose applications on Logos, with built-in support for private execution. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### `lgpm`\n\nThe official package manager used for Logos modules. See [Package](#package).\n\n### `lgpd`\n\nThe official package downloader for Logos modules. See [Package](#package).\n\n### `.lgx`\n\nThe file extension for a Logos [package](#package): a gzip-compressed archive bundling a module's platform-specific binaries with a manifest, loadable by Basecamp.\n\n### Light node\n\nA resource-limited device or client that leverages service nodes to access the Logos Delivery Relay network.\n\n### Light push\n\nA protocol enabling Logos Delivery light nodes to send messages to the Relay network and receive acknowledgements confirming that a peer has received them. See [Understand Logos Delivery protocols](https://docs.logos.co/messaging/concepts/protocols.md#light-push).\n\n### LIP\n\n**Logos Improvement Proposal** - a proposed community update to the Logos design, which may or may not be incorporated into the project.\n\n### `lm`\n\nA helper tool for Logos modules.\n\n### Locator\n\nAn address assigned to a Logos Blockchain node participating in the Blend Network. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-declaration-protocol).\n\n### Logos Blockchain\n\nThe foundational infrastructure layer of the Logos technology stack. See [Introduction to the Logos Blockchain](https://docs.logos.co/blockchain.md).\n\n### Logos Chat\n\nA library for private, encrypted peer-to-peer chat using Logos Delivery. A component of Logos Delivery.\n\n### Logos Delivery\n\nA peer-to-peer messaging network with DoS protection via Rate Limiting Nullifiers. A component of Logos Delivery.\n\n### Logos Execution Environment\n\nThe virtual machine that runs on the Logos Execution Zone. Also known as the **LEE**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### Logos Execution Zone\n\nThe primary execution layer for general-purpose applications on Logos, with built-in support for private execution. Also known as the **LEZ**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### Logos Messaging\n\nThe private messaging layer of the Logos technology stack.\n\n### Logos node\n\nA node running one or several Logos modules that connect to a network of other such nodes. See [Run a Logos node with blockchain, storage, and delivery](https://docs.logos.co/run-a-node.md).\n\n### `logos-module-builder`\n\nA build tool for Logos modules.\n\n### `logos-module-viewer`\n\nA graphical tool for inspecting Logos modules.\n\n### `logos-standalone-app`\n\nAn app that displays UI views for Logos modules.\n\n### Logos Storage\n\nThe privacy-preserving filesharing component of the Logos technology stack.\n\n### `logoscore`\n\nA daemon that runs Logos modules from the command line.\n\n### LSSA\n\nAn old name for the Logos Execution Environment. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n---\n\n## M\n\n### Mantle\n\nThe execution layer of Bedrock, provides operations that allow nodes to participate in the Blend Network, as well as an execution environment that allows for interactions with Zones. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md).\n\n### Message\n\nA virtual \"block\" of data that forms part of a Logos channel. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md).\n\n### Mining\n\nThe process of obtaining notes via Proof of Work. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md#mining-notes).\n\n### Mix\n\nTraffic routing used to improve privacy properties.\n\n### Mixnet\n\nA privacy-preserving network layer, which uses traffic routing to improve privacy properties. See [Mixnet](https://docs.logos.co/mixnet/get-started/send-anonymised-messages-over-the-mix-network.md).\n\n### Module\n\nA self-contained, reusable software component that provides a specific capability. Multiple modules can be loaded and made to interact to form a complete application. A module is typically distributed as a [package](#package).\n\n### Mostly offline\n\nLogos Delivery clients who spend most of their time offline or disconnected from the internet and only occasionally to the network. Examples include browsers and mobile phones.\n\n---\n\n## N\n\n### Nescience\n\nAn old name for the Logos Execution Environment. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### Nomos\n\nAn old name for the Logos Blockchain. See [Introduction to the Logos Blockchain](https://docs.logos.co/blockchain.md).\n\n### Node key\n\nA Secp256k1 (64-char hex string) private key for generating the PeerID, listening addresses, and discovery addresses of a Logos Delivery node.\n\n### Note\n\nA fungible UTXO token used on Mantle. See [About Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md#mantle-ledger).\n\n### NPK\n\nA **Nullifier Public Key** - a public key used to verify token ownership on the LEZ. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### NSSA\n\nAn old name for the Logos Execution Environment. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### NSK\n\nA **Nullifier Secret Key** - a secret key used to sign LEZ transactions. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Nullifier keys\n\nA key pair used to sign transactions and verify token ownership on the LEZ. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Nullifier Public Key\n\nA public key used to verify token ownership on the LEZ, also known as an **NPK**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Nullifier Secret Key\n\nA secret key used to sign LEZ transactions, also known as an **NSK**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n---\n\n## O\n\n### Out-of-band\n\nExchanging information through a separate, secure channel distinct from the main communication method to enhance security.\n\n---\n\n## P\n\n### Package\n\nThe distributable unit for a Logos [module](#module): a `.lgx` archive bundling the module's platform-specific binaries with a manifest describing its metadata, dependencies, and optional signature. A [catalogue](#catalogue) lists packages, and `logosctl`/`lgpm`/`lgpd` install them. See [LGX package format and bundling reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md).\n\n### PDA\n\n**Program Derived Address** - the address assigned to an LEE program. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### Peer discovery\n\nWhen a Logos Delivery node locates and gets information about other peers in the network. See [Understand peer discovery](https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-discovery.md).\n\n### Peer exchange\n\nA peer discovery mechanism used in Logos Delivery that enables light nodes to request and receive peers from other nodes in the network, allowing them to bootstrap and expand their connections without depending on Discv5. See [About peer exchange](https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-exchange.md).\n\n### Peer ID\n\nThe unique identifier of a node in the Logos Delivery network generated from the cryptographic hash of the node's public key.\n\n### PoL\n\n**Proof of Leadership** - the ZK proof that a note's owner is eligible to propose a block. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md).\n\n### PPoS\n\n**Private Proof of Stake** - a Proof of Stake consensus mechanism that protects the identity of block proposers. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md).\n\n### Private account\n\nA type of LEZ account that keeps its transactions private. Account updates are proven via ZK proofs. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Private Proof of Stake\n\nA Proof of Stake consensus mechanism that protects the identity of block proposers, also known as **PPoS**. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md).\n\n### Program\n\nAn executable smart contract on the LEZ, associated with an LEZ account. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Program Derived Address\n\nThe address assigned to an LEZ program, also known as a **PDE**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md).\n\n### Proof of Leadership\n\nThe ZK proof that a note's owner is eligible to propose a block. Also known as **PoL**. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md).\n\n### Proposer privacy\n\nThe difficulty of linking a block proposer on the Logos Blockchain to the block they proposed. See [Blockchain FAQ](https://docs.logos.co/blockchain/faq.md#how-is-the-logos-blockchain-private-if-mantle-notes-are-public-utxos).\n\n### Public account\n\nA type of LEZ account whose state is publicly available. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n---\n\n## R\n\n### Relay\n\nA protocol that extends the GossipSub protocol to enable secure and censorship resistant message sending and receiving among peers while preserving privacy. It also scales the Logos Delivery network to accommodate many nodes efficiently. See [Understand Logos Delivery protocols](https://docs.logos.co/messaging/concepts/protocols.md#relay).\n\n---\n\n## S\n\n### SDP\n\nThe **Service Declaration Protocol** - used to keep track of Logos Blockchain nodes that opted in to participate in the Blend Network. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-declaration-protocol).\n\n### Sealing keys\n\nA key pair used by users of a shared LEZ account, together with the Group Master Secret, to derive a local instance of a shared account. See [Set up a shared private LEZ account](https://docs.logos.co/lez/accounts/set-up-shared-private-lez-account.md).\n\n### Service\n\nAn opt-in protocol of the Logos Blockchain that requires a known and agreed-upon list of participants to function. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-declaration-protocol).\n\n### Service Declaration Protocol\n\nUsed to keep track of Logos Blockchain nodes that opted in to participate in the Blend Network. Also known as the **SDP**. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-declaration-protocol).\n\n### Service Reward Distribution Protocol\n\nEnables reward distribution to Logos Blockchain nodes participating in the Blend Network. Also known as the **SRDP**. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-reward-distribution-protocol).\n\n### Slot\n\nThe basic time unit used by the Logos Blockchain, equivalent to 1 second. See [About Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md#time-units).\n\n### SRDP\n\nThe **Service Reward Distribution Protocol** - enables reward distribution to Logos Blockchain nodes participating in the Blend Network. See [About the Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-reward-distribution-protocol).\n\n### Storage module\n\nThe Logos module that provides filesharing capabilities via Logos Storage.\n\n### Store\n\nA protocol that enables the storage of relayed messages in the Logos Delivery network, allowing offline peers to retrieve missed messages upon reconnecting to the network. See [Understand Logos Delivery protocols](https://docs.logos.co/messaging/concepts/protocols.md#store).\n\n---\n\n## T\n\n### Token definition account\n\nAn LEZ account that defines the properties and behaviour of a custom token. See [Create and transfer custom tokens on the LEZ](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md).\n\n### Token holding account\n\nAn LEZ account that is able to hold a custom token. See [Create and transfer custom tokens on the LEZ](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md).\n\n### Token program\n\nAn LEZ program that provides the functionality for the creation of custom LEZ tokens. See [Create and transfer custom tokens on the LEZ](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md).\n\n### Transport\n\nA network mechanism that establishes connections between Logos Delivery peers and enables efficient transmission, routing, and delivery of data packets.\n\n---\n\n## U\n\n### UI Module\n\nA Logos module that includes a user interface view.\n\n---\n\n## V\n\n### Viewing keys\n\nA key pair used to generate and verify ZK proofs on the LEZ. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Viewing Public Key\n\nA public key used to verify ZK proofs on the LEZ, also known as a **VPK**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### Viewing Secret Key\n\nA secret key used to generate ZK proofs on the LEZ, also known as a **VSK**. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### VPK\n\nA **Viewing Public Key** - a public key used to verify ZK proofs on the LEZ. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n### VSK\n\nA **Viewing Secret Key** - a secret key used to generate ZK proofs on the LEZ. See [Introduction to the Logos Execution Zone](https://docs.logos.co/lez.md#accounts-model).\n\n---\n\n## W\n\n### Waku\n\nAn old name for the Logos Delivery component of the Logos technology stack.\n\n---\n\n## Z\n\n### Zone SDK\n\nAn SDK provided by Logos to facilitate the creation of custom Logos Zones. See [About Zones](https://docs.logos.co/blockchain/concepts/about-zones.md).\n\n### Zone\n\nAn L2 blockchain that defines its own state and execution environment but uses the Logos Blockchain for consensus and data availability. See [About Zones](https://docs.logos.co/blockchain/concepts/about-zones.md).\n"},{"id":"get-started/testnet-endpoints","title":"Testnet endpoints","summary":"Look up the RPC, explorer, release, and channel for each Logos Testnet.","section":"get-started","doc_type":"reference","url":"https://docs.logos.co/get-started/testnet-endpoints","markdown_url":"https://docs.logos.co/get-started/testnet-endpoints.md","permalink":"/get-started/testnet-endpoints","source":"https://github.com/logos-co/logos-docs/blob/main/docs/get-started/testnet-endpoints.md","headings":[{"depth":2,"text":"Endpoints by version"},{"depth":2,"text":"Related information"}],"markdown":"# Testnet endpoints\n\n#### Look up the RPC, explorer, release, and channel for each Logos Testnet.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis page lists the public endpoints for each Testnet version, so you can find the right one when a procedure refers to the testnet sequencer or explorer. Use the row that matches the version your procedure targets.\n\n## Endpoints by version\n\n| Version | LEZ RPC | LEZ explorer | Blockchain explorer | LEZ release | Blockchain channel |\n|:---|:---|:---|:---|:---|:---|\n| 0.4 (actor model) | TBD | TBD | TBD | TBD | TBD |\n| 0.3 (current) | `https://testnet.lez.logos.co` | `https://explorer.testnet.lez.logos.co/` | `https://testnet.blockchain.logos.co/web/explorer/` | [`v0.3.1`](https://github.com/logos-blockchain/logos-execution-zone/releases/tag/v0.3.1) | [`0101010101010101010101010101010101010101010101010101010101010101`](https://testnet.blockchain.logos.co/web/explorer/channel/0101010101010101010101010101010101010101010101010101010101010101) |\n| 0.2.4 (legacy) | `https://testnet-legacy.lez.logos.co/` | `https://explorer.testnet-legacy.lez.logos.co/` | `https://testnet.blockchain.logos.co/web/explorer/` | [`v0.2.4-lez-legacy`](https://github.com/logos-blockchain/logos-execution-zone/releases/tag/v0.2.4-lez-legacy) | [`0240240240240240240240240240240240240240240240240240240240240240`](https://testnet.blockchain.logos.co/web/explorer/channel/0240240240240240240240240240240240240240240240240240240240240240) |\n\n## Related information\n\n- [Run the LEZ wallet app and initiate native token transfers](https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md)\n- [Run a Blockchain node from the CLI](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md)\n"},{"id":"basecamp/install-logos-basecamp","title":"Install Logos Basecamp","summary":"Get Logos Basecamp running on your desktop.","section":"basecamp","doc_type":"procedure","url":"https://docs.logos.co/basecamp/install-logos-basecamp","markdown_url":"https://docs.logos.co/basecamp/install-logos-basecamp.md","permalink":"/basecamp/install-logos-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/basecamp/install-logos-basecamp.md","headings":[{"depth":2,"text":"Install from a prebuilt release"},{"depth":3,"text":"Apps available on Windows"},{"depth":2,"text":"Build and run Logos Basecamp from source"},{"depth":2,"text":"Troubleshooting Basecamp"},{"depth":3,"text":"Experimental Nix feature 'nix-command' when building the AppImage"},{"depth":3,"text":"I see an `error while loading shared libraries` error when trying to launch the AppImage on Linux?"}],"markdown":"# Install Logos Basecamp\n\n#### Get Logos Basecamp running on your desktop.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.1**.\n\nLogos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) is a local-first launcher for peer-to-peer apps built on Logos. You can discover, install, and run Logos [modules](https://docs.logos.co/get-started/glossary.md#module) and [apps](https://docs.logos.co/get-started/glossary.md#app) using its graphical interface as an alternative to the command line.\n\nYou can install Logos Basecamp in two ways:\n\n| Method | When to use it | Tooling required |\n|:--|:--|:--|\n| [Prebuilt release (AppImage, DMG or Windows installer)](#install-from-a-prebuilt-release) | End users | None |\n| [Build from source with Nix](#build-and-run-logos-basecamp-from-source) | Contributors, custom builds, unsupported platforms | Nix with flakes enabled |\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux x86_64 or aarch64 (tested on Ubuntu 22.04+)\n>     - macOS aarch64 (recent versions)\n>     - Windows 10 or 11 x86_64 (native installer; see [apps available on Windows](#apps-available-on-windows))\n>     - WSL2 Ubuntu on Windows 11 (or Windows 10 21H2+ with WSLg for GUI support) is also supported\n> - 4 GB RAM minimum (8 GB recommended) and ~2 GB free disk space.\n> - For the source build only: **Nix** with flakes enabled.\n>     - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>     ```bash\n>     mkdir -p ~/.config/nix\n>     echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>     ```\n\n## Install from a prebuilt release\n\n1. Go to the [latest release page](https://github.com/logos-co/logos-basecamp/releases/latest) and download the artifact for your OS:\n    - Linux, WSL: the `AppImage` files\n    - macOS: the `.dmg` files\n    - Windows: the `-x86_64-setup.exe` file\n\n1. Depending on your OS, install and launch Basecamp as follows:\n\n- On macOS, open the `.dmg` file and drag the Basecamp app inside it into `/Applications`. Then launch Basecamp from `/Applications`.\n- On Linux and WSL, install some prerequisites, then grant execute permission to the downloaded AppImage and launch it:\n\n    ```bash\n    # install rerequisites\n    sudo apt-get install -y libfuse2t64 libegl1 libopengl0\n    # On Ubuntu 22.04 and earlier, use libfuse2 instead of libfuse2t64\n\n    chmod +x LogosBasecamp-Desktop-*.AppImage\n    ./LogosBasecamp-Desktop-*-x86_64.AppImage  # or ./LogosBasecamp-Desktop-*-aarch64.AppImage\n    ```\n\n    The file name carries the release version and commit, for example `LogosBasecamp-Desktop-v0.3.2-f2fae6-x86_64.AppImage`. If you have downloaded more than one release, run the file name in full.\n\n- On Windows, run the downloaded `LogosBasecamp-Desktop-*-x86_64-setup.exe`. It doesn't need administrator rights.\n    - The installer is not code-signed yet, so Microsoft Defender SmartScreen may show **Windows protected your PC**. Click **More info**, then **Run anyway**.\n    - Choose **Install (recommended)** to install for your user under `%LOCALAPPDATA%\\Programs\\Logos Basecamp`, with a Start menu entry, an optional desktop shortcut, and an uninstaller in **Settings > Apps**. Choose **Portable (extract only)** to only extract the files to a folder of your choice.\n    - Launch **Logos Basecamp** from the Start menu, or run `bin\\LogosBasecamp.exe` from the portable folder.\n\n### Apps available on Windows\n\nNot every module in the catalogue has a Windows build yet. Of the apps covered in these docs, these run on native Windows:\n\n- **Chat**: the `chat_ui`, `chat_module`, and `delivery_module` [packages](https://docs.logos.co/get-started/glossary.md#package). See [Send 1:1 messages with the Logos Chat app](https://docs.logos.co/messaging/get-started/send-1-1-messages-logos-chat.md).\n- **Storage**: the `storage_ui` and `storage_module` packages. See [Run a Storage Node from Basecamp](https://docs.logos.co/storage/get-started/run-storage-node-from-basecamp.md).\n\nFor other apps, such as the blockchain node or the swap app, run the Linux AppImage under WSL2.\n\n## Build and run Logos Basecamp from source\n\n1. Clone the repository and enter it:\n\n    ```bash\n    git clone https://github.com/logos-co/logos-basecamp.git\n    cd logos-basecamp\n    ```\n\n1. Build Basecamp with Nix flake:\n\n    ```bash\n    nix build '.#bin-appimage'\n    ```\n\n1. Run the resulting binary:\n\n    ```bash\n    ./result/logos-basecamp.AppImage\n    ```\n\n## Troubleshooting Basecamp\n\n### Experimental Nix feature 'nix-command' when building the AppImage\nThe following error might appear while building the AppImage:\n\n```text\nerror: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it\n```\n\nin which case the command should be changed to enable flakes:\n\n```bash\nnix build --extra-experimental-features 'nix-command flakes' '.#bin-appimage'\n```\n\n### I see an `error while loading shared libraries` error when trying to launch the AppImage on Linux?\nThe AppImage relies on a few graphics and font libraries from your system. On a minimal Ubuntu install, such as WSL or a server image, some of them may be missing, for example `libEGL.so.1`, `libOpenGL.so.0`, or `libharfbuzz.so.0`. Install them with:\n\n```bash\nsudo apt-get install -y libfuse2t64 libegl1 libopengl0 libharfbuzz0b\n# On Ubuntu 22.04 and earlier, use libfuse2 instead of libfuse2t64\n```\n\nDon't install the `fuse` package on Ubuntu 22.04 or later. It replaces `fuse3`, which other system packages depend on.\n"},{"id":"basecamp/install-and-load-module-in-basecamp","title":"Install and load a module in Basecamp","summary":"Access features and functionalities through modules in Logos Basecamp.","section":"basecamp","doc_type":"procedure","url":"https://docs.logos.co/basecamp/install-and-load-module-in-basecamp","markdown_url":"https://docs.logos.co/basecamp/install-and-load-module-in-basecamp.md","permalink":"/basecamp/install-and-load-module-in-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/basecamp/install-and-load-module-in-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install the module package"},{"depth":3,"text":"Install from the online catalogue"},{"depth":3,"text":"How Basecamp downloads a package"},{"depth":3,"text":"Choose the download source"},{"depth":3,"text":"Install from a local `.lgx` file"},{"depth":2,"text":"Step 2: Load the module"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"The installed module doesn't appear in Module Inspector or Apps Inspector"},{"depth":3,"text":"A package downloads over HTTPS instead of Logos Storage"},{"depth":3,"text":"A QML-based UI App cannot reach the network"}],"markdown":"# Install and load a module in Basecamp\n\n#### Access features and functionalities through modules in Logos Basecamp.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nIn Logos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp), you can install and load modules that provide features like chat, storage, or wallets from the online [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue) or local `.lgx` files.\n\nThere are two types of modules in Logos Basecamp. [Core modules](https://docs.logos.co/get-started/glossary.md#core-module) are the headless background services that provide capabilities like messaging or storage, while [UI modules](https://docs.logos.co/get-started/glossary.md#ui-module) are the visual front-ends users interact with. Both kinds of modules are packaged and distributed as [packages](https://docs.logos.co/get-started/glossary.md#package).\n\n> **Info: Prerequisites**\n>\n>\n> - [Basecamp installed and running](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - Internet access for online catalogue install.\n> - [An `.lgx` file](https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module.md) for local install. Make sure that the archive contains a variant matching your platform.\n\n## What to expect\n\n- You can install a [module](https://docs.logos.co/get-started/glossary.md#module) from the online catalogue or from a local `.lgx` file.\n- You can choose where Basecamp downloads packages from: the [Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) network, HTTPS, or either.\n- You can load or unload a module from **Settings**: core modules under **Module Inspector**, UI modules under **Apps Inspector**.\n- You can read a loaded module's status, CPU, and memory in **Settings > Module Inspector**. **Apps Inspector** shows a UI module's status and version, without live resource usage.\n\n> **Tip**\n>\n> An introduction to modules in Logos Basecamp is available in video form:\n> [Video: Understanding .lgx modules on Logos Basecamp](https://www.youtube.com/watch?v=yZ_93uAAY9A)\n\n## Step 1: Install the module package\n\nWhen installing a module, Logos Basecamp extracts the variant for your platform into your user modules directory (core modules) or user plugins directory (UI modules).\n\n### Install from the online catalogue\n\n1. At the bottom of the sidebar, click **Package Manager**.\n1. Browse and locate the module you want. Use the **Categories** and **Types** filters or the search box. The **Type** column shows `core` for core modules and `ui_qml` for UI modules. Click a row to open its **Details** panel on the right.\n1. Click **INSTALL** on the module's row. An **Install Package?** dialogue opens and lists any dependency changes. Click **Install** to confirm.\n1. Wait until the row's **Action** column reads `INSTALLED`. Core modules then appear in **Settings > Module Inspector**. UI modules appear in the sidebar and in **Settings > Apps Inspector**.\n\n### How Basecamp downloads a package\n\nBasecamp ships with the `storage_module` module, which runs a Logos Storage node. A catalogue can publish a package version on Logos Storage with a `logos:` address, as well as over HTTPS. \n\n- When a package with the desired version is available on the Logos Storage network and the Storage node is running, Basecamp downloads the package from Logos Storage.\n- When the Storage node is stopped, `storage_module` is not loaded, or the node is on another network than the catalogue, Basecamp downloads the package from the HTTPS URL.\n- If the Logos Storage download takes too long, Basecamp stops it and downloads the package from the HTTPS URL.\n\nThe **Source** column of **Package Manager** shows where a package comes from. For a package that is not installed, it shows an icon for each source from which the package is available for download. For an installed package, it shows the source it was downloaded from. Hover over an icon to see the name of the package source.\n\n![The Source column in Package Manager](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/basecamp/assets/install-and-load-a-module-in-logos-basecamp/package-manager-source.png)\n\n### Choose the download source\n\n1. At the bottom of the sidebar, click **Settings** ![](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/basecamp/assets/install-and-load-a-module-in-logos-basecamp/settings-icon.png), then click **Package Repositories**.\n1. Under **Download source**, pick one value:\n    - **Any**: Logos Storage when possible, HTTPS otherwise.\n    - **Logos only**: only Logos Storage.\n    - **HTTP only**: only the HTTPS URL.\n\n    ![The Download source selector in Package Repositories](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/basecamp/assets/install-and-load-a-module-in-logos-basecamp/download-source.png)\n\nIf the selected source cannot serve a version, **Package Manager** shows it as **Not available** and you cannot install it.\n\n### Install from a local `.lgx` file\n\n1. At the bottom of the sidebar, click **Package Manager**.\n1. Click **Install Local Package**.\n1. Select the `.lgx` file, click **Open**, then click **Install** in the **Install Package?** dialogue.\n1. Core modules appear in **Settings > Module Inspector**. UI modules appear in the sidebar and in **Settings > Apps Inspector**.\n\n## Step 2: Load the module\n\nLoading a module turns an installed module into a running service you can actually use. Each loaded Logos module runs in its own `logos_host` process, so memory usage increases with the number of loaded modules.\n\n1. At the bottom of the sidebar, click **Settings** ![](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/basecamp/assets/install-and-load-a-module-in-logos-basecamp/settings-icon.png), then click **Module Inspector**, or **Apps Inspector** for a UI module.\n1. Find the module in the list. Its **Status** column reads `NOT LOADED`.\n1. Click **Load** next to the module. The status changes to `LOADED` and the **CPU** and **Memory** columns start reporting.\n\nTo open a UI module, click its icon in the sidebar.\n\n> **Info**\n>\n> You can click **Unload** in **Settings > Module Inspector** or close the tab of a module to unload it. Unloading stops the module's host process but not its dependencies, which may still be in use by other modules or UI Apps.\n\n## Troubleshooting\n\n### The installed module doesn't appear in Module Inspector or Apps Inspector\n\nConfirm the module actually installed: check its status in **Package Manager**.\n\n- If installing from the online catalogue: if a package's row shows **Not available** instead of **Install**, it has no build for your platform, build type (dev, portable, or release), or architecture, or the [download source](#choose-the-download-source) you selected cannot serve it.\n- If installing from a local `.lgx` file: Basecamp reports an error if the archive has no variant for your platform.\n\nEither way, confirm the archive includes a variant matching your platform before reinstalling. If the package shows as installed in Package Manager but still doesn't appear in the relevant inspector, its manifest `type` may not match where you're looking—core modules only appear in Module Inspector, and UI modules (`ui_qml`) only appear in Apps Inspector.\n\n### A package downloads over HTTPS instead of Logos Storage\n\nBasecamp uses Logos Storage only when the Logos Storage node can serve the package.\n\n1. Open **Settings > Package Repositories** and check that **Download source** is not **HTTP only**.\n1. Open **Settings > Module Inspector** and check that **Storage Module** (`storage_module`) reads `LOADED`. If it doesn't, click **Load**.\n\n    ![Storage Module loaded in Module Inspector](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/basecamp/assets/install-and-load-a-module-in-logos-basecamp/module-inspector-storage.png)\n\n1. If you installed the **Storage** app, open it and check that the node is running. A package download will not begin if your Storage node is stopped.\n\n### A QML-based UI App cannot reach the network\n\nBy design, QML UI Apps run inside a sandboxed QML engine with a deny-all QNetworkAccessManager and a URL interceptor that whitelists only the app's own directory. To make network calls, route them through a Logos Module, which runs in its own unsandboxed process. Use `logos.callModule(\"<module>\", \"<method>\", [args])` from QML.\n"},{"id":"basecamp/swap-eth-and-lez-tokens-in-basecamp","title":"Swap ETH and LEZ tokens in Basecamp","summary":"Install the atomic swap app from a catalogue URL and trade Sepolia ETH for LEZ testnet tokens with a counterparty you never have to trust.","section":"basecamp","doc_type":"procedure","url":"https://docs.logos.co/basecamp/atomic-swaps-poc","markdown_url":"https://docs.logos.co/basecamp/atomic-swaps-poc.md","permalink":"/basecamp/atomic-swaps-poc","source":"https://github.com/logos-co/logos-docs/blob/main/docs/basecamp/swap-eth-and-lez-tokens-in-basecamp.md","headings":[{"depth":2,"text":"Networks and addresses"},{"depth":2,"text":"How an atomic swap works"},{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Add the catalogue and install the app"},{"depth":2,"text":"Step 2: Set up your accounts"},{"depth":3,"text":"Get LEZ tokens to sell"},{"depth":2,"text":"Step 3: Confirm your configuration"},{"depth":2,"text":"Step 4: Take a live offer"},{"depth":2,"text":"Step 5: Read your receipt and verify it"},{"depth":2,"text":"Step 6: Send feedback"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"The app doesn't appear after installing it"},{"depth":3,"text":"The Market tab is empty"},{"depth":3,"text":"The app can't connect to Ethereum"},{"depth":3,"text":"The swap does nothing and no error appears"},{"depth":3,"text":"The swap stalls on `Wait for the seller`"},{"depth":3,"text":"There aren't enough LEZ tokens in the account"},{"depth":3,"text":"Setup shows a `Fund LEZ` step instead"},{"depth":3,"text":"A swap stopped halfway and the funds are still locked"},{"depth":2,"text":"Next steps"}],"markdown":"# Swap ETH and LEZ tokens in Basecamp\n\n#### Install the atomic swap app from a catalogue URL and trade Sepolia ETH for LEZ testnet tokens with a counterparty you never have to trust.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThe atomic swap [app](https://docs.logos.co/get-started/glossary.md#app) is a Logos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) app that trades tokens across two unrelated chains without an exchange, a bridge, or an escrow agent. This procedure takes you from a fresh Basecamp install to a completed swap against a live counterparty on the testnet, ending with a receipt you can check on both chains' block explorers.\n\nYou install this app from a [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue) URL rather than building it. The app also sets up both of your accounts for you: a guided **Setup** tab generates your Ethereum key, then creates and activates your [LEZ](https://docs.logos.co/get-started/glossary.md#lez) [account](https://docs.logos.co/get-started/glossary.md#account), and finishes by pointing you at a faucet for the Sepolia gas it can't fetch on your behalf, all in [Step 2](#step-2-set-up-your-accounts). You don't need to hold any LEZ tokens before you start.\n\n## Networks and addresses\n\nThe app ships with these values already filled in. They're listed here so you can check them, and so you can restore one if you change it by mistake.\n\n| Setting | Value |\n|:--|:--|\n| Catalogue URL | `https://raw.githubusercontent.com/logos-co/eth-lez-atomic-swaps/master/logos-repo.json` |\n| Ethereum RPC | `wss://ethereum-sepolia-rpc.publicnode.com` |\n| Ethereum HTLC contract, on Sepolia | `0x351B0EA07739FA9F6769213927D7836a790A5FAF` |\n| LEZ sequencer | `https://testnet.lez.logos.co` |\n| LEZ HTLC program | `9eb88f51aae87a58fb74b8d2dc7327b39333585e63280e3f9cf8d86dac0ed702` |\n| Ethereum explorer | `https://sepolia.etherscan.io/tx/<TX_HASH>` |\n| LEZ explorer | `https://explorer.testnet.lez.logos.co/transaction/<TX_HASH>` |\n\n> **Warning**\n>\n> The Ethereum RPC must use the `wss://` scheme. The app subscribes to contract events over a WebSocket, so an `https://` URL fails to connect even though the same host serves ordinary calls happily.\n\n## How an atomic swap works\n\nA swap is a trade between two people who have no reason to trust each other. One holds LEZ testnet tokens, the other holds Sepolia ETH, and neither wants to send first. An atomic swap removes that problem: either both transfers happen, or neither does.\n\nThe mechanism is a hash time-locked contract, or HTLC. Your app invents a random secret, called a preimage, and locks your ETH in a contract on Sepolia that releases only to whoever presents that secret. Your counterparty sees the lock and makes a matching lock of their LEZ tokens against the *hash* of the same secret. You then claim their LEZ tokens, which publishes the secret on the LEZ as a side effect. Your counterparty reads it there and uses it to claim your ETH. Both legs settle, and neither of you ever handed over funds on trust.\n\nThe time locks make the failure case safe. Each lock carries a deadline, and yours is deliberately the longer of the two, so you always have time to react to whatever your counterparty does. If they vanish after you lock, nothing is lost. You wait for your deadline to pass and take your own funds back from the **Refund** tab. In the offer you'll take below, the LEZ side expires after 20 minutes and your Ethereum side after 40.\n\n> **Info: Prerequisites**\n>\n>\n> - [Basecamp installed and running](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - Internet access.\n> - A small amount of **Sepolia ETH**, sent to the throwaway Ethereum address the app generates for you in [Step 2](#step-2-set-up-your-accounts). The trade itself costs `0.00001` ETH, so roughly `0.01` Sepolia ETH covers it and the gas comfortably. The app can activate your LEZ account for you but cannot fund your Ethereum side, so this part is genuinely required: without it you cannot complete a swap.\n>     - **Setup** offers `https://sepolia-faucet.pk910.de/` for this, and any other [public Sepolia faucet](https://ethereum.org/en/developers/docs/networks/) works too. Most faucets just ask for the destination address, so you don't need a separate wallet app.\n> - **No LEZ.** You don't need any to take the offer in this procedure: the LEZ network charges no fees, so a buyer can start from an empty LEZ balance and acquire LEZ tokens from the market. Selling LEZ tokens requires a balance up front, which is covered in [Get LEZ tokens to sell](#get-lez-tokens-to-sell).\n\n## What to expect\n\n- You can add a third-party catalogue to Basecamp and install an app from it.\n- You can use the app's guided **Setup** tab to generate an Ethereum key, to create and activate a LEZ account, and to get that Ethereum key funded, with no command line and no hand-copied keys.\n- You can take a live offer and complete a real cross-chain swap on public test networks.\n- You can verify both legs of your swap independently on two block explorers.\n\n## Step 1: Add the catalogue and install the app\n\nBasecamp arrives with the official Logos catalogue configured, and it merges that built-in catalogue with any you add yourself. The atomic swap app is published from its own repository, so you add its catalogue first. A catalogue is a small JSON file naming an index of [packages](https://docs.logos.co/get-started/glossary.md#package), and Basecamp re-reads it whenever the index changes.\n\n1. In the sidebar, click **Package Manager**, then click **Manage Repositories** in the toolbar.\n\n    This opens **Settings** at the **Package Repositories** page, which lists the repositories you're drawing packages from.\n\n1. Under **Add a repository**, paste this URL into the field and click **Add**:\n\n    ```text\n    https://raw.githubusercontent.com/logos-co/eth-lez-atomic-swaps/master/logos-repo.json\n    ```\n\n    **Expected:** a repository named **ETH ↔ LEZ Atomic Swaps** joins the list, alongside the built-in one marked `Default`. No restart is needed. Basecamp re-reads the catalogue as soon as the repository is added.\n\n    > **Info**\n    >\n    > If the new row shows an `Unreachable` badge, the URL is wrong or your network blocked the request. Click **Refresh** to retry. The field expects the URL of a `logos-repo.json` index, not a link to the repository's web page.\n\n1. Go back to **Package Manager** and search for `swap`.\n\n    **Expected:** two packages from the new repository, `swap` and `swap_ui`, both shown as **ETH ↔ LEZ Atomic Swap** and both at the same version. Tell them apart by the **Type** column: `core` is `swap` and `ui_qml` is `swap_ui`. The catalogue always serves the current release, so take whatever version it offers rather than looking for a particular number.\n\n1. Install `swap` first, then install `swap_ui`.\n\n    Click **INSTALL** on the package's row. An **Install Package?** dialogue opens. Confirm with **Install** and wait for the row's **Action** column to read `INSTALLED`.\n\n    > **Warning**\n    >\n    > Install `swap` before `swap_ui`. The UI package declares a dependency on the backend, and taking them the other way round leaves the interface with no backend to talk to.\n\n1. Open **ETH ↔ LEZ Atomic Swap** from the sidebar, where its tile reads `SW`. If the tile isn't there, restart Basecamp.\n\n    **Expected:** a row of six tabs across the top: **Market**, **Swap**, and **History**, then, after a divider, **Sell**, **Refund**, and **Setup**. Beneath the tabs runs a strip with your `ETH` and `LEZ` addresses and balances, which keep themselves up to date, and a connection chip that settles on `Connected` once the app finds a peer, gaining a peer count—`Connected · 1 peer` and up—as it counts them.\n\n    On a fresh install, with nothing configured yet, the app opens on the **Setup** tab for you. That's the next step.\n\n> **Info**\n>\n> The catalogue is saved in your Basecamp settings and survives restarts. You add it once.\n\n## Step 2: Set up your accounts\n\nA swap needs two identities: an Ethereum key to sign your Sepolia transactions, and an activated LEZ account to receive your tokens. Setup doesn't hand you LEZ tokens, and doesn't need to: you buy that on the **Market** tab in [Step 4](#step-4-take-a-live-offer). The app builds both identities for you in the **Setup** tab, with no command line and no copying a raw private key between apps. Every field the tab fills is an ordinary configuration field underneath, listed in the tab's own **Advanced settings** section, so nothing here is hidden from you.\n\nOn a fresh install the app opens on the **Setup** tab automatically. You can also reach it any time from the **Setup** tab at the right-hand end of the tab row.\n\n> **Info**\n>\n> The Ethereum key the app generates is a fresh, throwaway key. Fund it with Sepolia test ETH only, and never treat it as a wallet for real funds.\n\n1. Open the **Setup** tab.\n\n    **Expected:** a page headed **Get set up**, subtitled `Four steps, then you're trading. No keys to type.`, with four numbered sections: **1. Ethereum key**, **2. LEZ account**, **3. Activate your LEZ account**, and **4. Get test ETH**. Each section's border turns green and its heading gains a `done` marker as you complete it.\n\n    Below them sits an unnumbered **Start trading** card. It isn't a step, because it asks nothing of you; it's the handoff to the market, and it stays dimmed until the four above it are done. Two more sections follow it: **Get test LEZ without trading**, collapsed and covered in [Get LEZ tokens to sell](#get-lez-tokens-to-sell) below, and **Advanced settings**, checked in [Step 3](#step-3-confirm-your-configuration). **Advanced settings** opens by itself on a fresh install, because the key fields it holds are still empty.\n\n1. Under **1. Ethereum key**, click **Generate a key**.\n\n    **Expected:** the section shows `done` and displays **Your address** followed by a new `0x…` address, with a copy button beside it. The app writes the matching private key straight into your configuration; you never see or paste it.\n\n1. Under **2. LEZ account**, click **Create an account**.\n\n    **Expected:** the section shows `done` and displays **Your account** followed by your new [account](https://docs.logos.co/get-started/glossary.md#account) ID. Nothing has reached the network yet. Creating the account is local, and the next section is what registers it.\n\n1. Under **3. Activate your LEZ account**, click **Activate account**.\n\n    Activation registers your account on the LEZ network so it can receive the tokens you buy. It's free, it's a single transaction, and it's the one step a buyer can't skip. LEZ tokens are obtained in [Step 4](#step-4-take-a-live-offer).\n\n    **Expected:** the button changes to **Activating…** and a status line appears with a live seconds counter, reading `Setting up your account on the network…` and then saying it's waiting for the network to confirm. Test-network blocks can be a minute or more apart, so the counter keeps moving to show it isn't stuck. When it finishes, the section shows `done`, the status line reads `Account set up on the network`, and the button becomes **Check again**.\n\n1. Under **4. Get test ETH**, copy the address shown under **Send it here** and send Sepolia ETH to it.\n\n    This is the one part that has to happen outside the app, because only you can fund your Ethereum address. The section names a faucet, `https://sepolia-faucet.pk910.de/`, behind a **Copy faucet link** button; Basecamp can't open it for you, so paste it into your browser yourself. Any other public Sepolia faucet works just as well.\n\n    **Expected:** while you wait, the section reads `Watching for it… you don't need to do anything.` and polls for the balance on its own. Once the ETH lands it shows `done` and a line reading `Arrived —` followed by the amount.\n\n1. Under **Start trading**, click **Go to Market**.\n\n    The button only enables once both **3. Activate your LEZ account** and **4. Get test ETH** are done, because a swap needs an activated account for the LEZ to land in and Sepolia ETH to pay the gas with. Until then the card tells you so: `Once you've activated your LEZ account (step 3) and test-ETH has landed, this is where you head to the market.`\n\n> **Info**\n>\n> Activation gets a step of its own because a LEZ account that was never activated is the most confusing failure in this app: the sequencer silently discards transactions that reference an account it has never seen initialised, so a swap simply stalls rather than failing. The app doesn't remember between launches whether your account is registered, so it always asks the network rather than guessing. If you set this account up in an earlier session, press **Activate account** anyway. It checks first and confirms in about a second without sending anything, and the same is true of **Check again** afterwards, which is the button to press if a swap ever does nothing at all.\n\n### Get LEZ tokens to sell\n\nYou don't need any LEZ tokens to buy LEZ tokens. Buying is what the app is for: you pay Sepolia ETH in the **Market** tab and obtain LEZ tokens. An empty LEZ balance is fine, because the LEZ testnet charges no fees.\n\nYou do need LEZ tokens to *sell*, because a sell offer has to be backed by LEZ tokens you already hold. Buy LEZ tokens on the **Market** tab first, then publish your sell offer from that balance. The collapsed **Get test LEZ without trading** section below **Start trading** no longer works on the testnet: its **Claim test LEZ** button fails, so leave it closed.\n\n## Step 3: Confirm your configuration\n\n**Advanced settings**, at the foot of the **Setup** tab, holds every endpoint, address, and key the app uses, grouped under **Ethereum**, **LEZ**, **Swap Parameters**, and **Developer**. It is the former **Config** tab, folded into **Setup** so there is one place to set the app up rather than two. After the guided sections above it, the key fields are already filled and the network values ship pre-filled, so this step is a check rather than a data-entry exercise. You can skip it and still complete a swap; it's here so you can see what **Setup** did and confirm nothing is off.\n\n1. Open the **Setup** tab and scroll to **Advanced settings**. Expand it if it's collapsed. It opens by itself when a field needs attention, so it's often open already, and clicking its heading then collapses it.\n\n1. Under **Ethereum**, confirm **RPC URL** is `wss://ethereum-sepolia-rpc.publicnode.com` and **HTLC Contract Address** is `0x351B0EA07739FA9F6769213927D7836a790A5FAF`.\n\n1. Still under **Ethereum**, confirm **Private Key** and **Recipient Address** are populated. The guided sections filled both from the key they generated in [Step 2](#step-2-set-up-your-accounts). **Recipient Address** is where your bought tokens' counterpart settles, so it's the address belonging to that same key.\n\n1. Under **LEZ**, confirm **Sequencer URL** is `https://testnet.lez.logos.co` and **HTLC Program ID** is `9eb88f51aae87a58fb74b8d2dc7327b39333585e63280e3f9cf8d86dac0ed702`.\n\n1. Still under **LEZ**, confirm **Signing Key** is populated. The guided sections filled it from the LEZ account they created in [Step 2](#step-2-set-up-your-accounts).\n\n    Leave **Wallet Home**, **Wallet Account ID**, and **Sell to one buyer only (optional)** empty. The app authenticates to the LEZ with the signing key, so the wallet fields aren't needed. **Sell to one buyer only (optional)** is a seller-side setting—the one account you'd allow to take your offers—and buying doesn't need it. **Wallet Home**'s placeholder, `.scaffold/wallet`, is a path from the app's development setup that doesn't exist on a machine that installed from the catalogue.\n\n1. Leave **Swap Parameters** alone.\n\n    These set the terms of offers you'd publish as a seller. When you take someone else's offer, the amounts and time locks come from that offer instead.\n\n1. Leave **Developer** alone too.\n\n    Its two buttons load credentials from a `.env` file next to a source checkout, which a catalogue install doesn't have.\n\n**Expected:** no red text under any field, and the **Market** tab's readiness chip reads `Ready to trade`. There's no save button. The app validates and saves as you type.\n\nIf a field is wrong, the app says so directly underneath it, with messages like `Required`, `Must be a 20-byte ETH address`, or `Must be a 32-byte hex program ID`. Fix those before continuing, because the app refuses to start a swap while any remain.\n\n> **Info**\n>\n> The contract at `0x351B0EA07739FA9F6769213927D7836a790A5FAF` is version 2 of the Ethereum HTLC. Earlier builds of this app pointed at a version 1 contract at `0x8636Fe66DFee166589a913140f14d5F57394834A`, which is still deployed and still responds. It's written out in full here so you can check character by character that you aren't on it, because the two are easy to confuse and the failure is unhelpful. Version 1 has no `INTERFACE_VERSION` function, so the app's compatibility check reverts against it and swaps don't start. If you've used this app before, replace the address with the one above.\n\n## Step 4: Take a live offer\n\nA maker runs on this testnet. It publishes offers and waits for someone to take them, which is what lets you complete this journey without a second machine and a friend.\n\n1. Open the **Market** tab.\n\n    **Expected:** a header reading `LIVE MARKET` with a count of the offers on the board, and a live tape with the columns `OFFER`, `RATE LEZ/ETH`, `SELLER`, `AGE`, and `EXPIRES`. A `next scan` bar beside the header drains between scans; the board rescans every two seconds by default.\n\n    When our maker is online, it advertises `10 LEZ` for `0.00001 ETH`, and that's the offer the rest of this step follows. The board can also be legitimately empty, because offers are live broadcasts rather than stored listings. If you see no offers, work through [The Market tab is empty](#the-market-tab-is-empty) and come back.\n\n1. Click the offer.\n\n    **Expected:** a detail pane on the right reading **Buy 10 LEZ** `for 0.00001 ETH`, with the rate beneath it, then the time left on both time locks and the rows **Seller ETH address**, **Seller LEZ account**, **LEZ program**, and **ETH contract**. You're buying LEZ tokens and paying in ETH. There's no **Hashlock** row yet: your app creates the secret, and with it the hashlock, only when you accept.\n\n1. Click the button reading `Accept — buy 10 LEZ`.\n\n    If the button is disabled, the app shows why just above it, such as `Finish setting up first — open Setup`, `This offer has expired`, or, while your Sepolia ETH hasn't arrived, `You don't have enough ETH for this swap`.\n\n1. Switch to the **Swap** tab and watch it run.\n\n    **Expected:** the progress stepper ticks through `Generate your secret`, `Lock your ETH`, `Your ETH is locked`, `Wait for the seller`, `Seller locked their LEZ`, `Check the escrow`, `Escrow checks out`, `Claim your LEZ`, and `LEZ claimed`. It usually takes one to three minutes, most of it waiting on Sepolia confirmations.\n\n1. Confirm the `LEZ` balance in the strip beneath the tabs has risen by `10`. It refreshes itself, so there's nothing to click.\n\n> **Info**\n>\n> Offers are live announcements, not stored listings. Nothing retains them, so the **Market** tab can only show what a maker is broadcasting at that moment. The board says as much under the tape: `Offers are advertisements — a swap completes only if the seller is still online.`\n\n## Step 5: Read your receipt and verify it\n\nEvery finished swap writes a receipt recording both legs, so you can check the trade against the two chains instead of taking the app's word for it. Receipts persist across restarts.\n\n1. Open the **History** tab and click your swap, then **Receipt ▾**.\n\n    **Expected:** a card headed `SWAP COMPLETED` with the hero line **Bought 10 LEZ**, followed by labelled rows including **Hashlock**, **Preimage**, **ETH lock tx**, **LEZ claim tx**, **ETH HTLC contract**, and both time locks. The preimage stays masked behind a **Reveal** toggle.\n\n1. On the **ETH lock tx** row, click the **↗** button, which copies a block-explorer link, then paste that link into your browser.\n\n    **Expected:** the button becomes a **✓**, and Etherscan shows a successful transaction against the HTLC contract. The URL looks like `https://sepolia.etherscan.io/tx/0x…`.\n\n1. Do the same on the **LEZ claim tx** row.\n\n    **Expected:** the LEZ explorer shows the matching claim. The URL looks like `https://explorer.testnet.lez.logos.co/transaction/` followed by a 64-character hash with no `0x` prefix.\n\n1. Look at the **Hashlock** on the receipt. It's the hash of the secret your app generated when you accepted the offer in [Step 4](#step-4-take-a-live-offer), and the row for this swap in the **History** list carries the same value.\n\n    The same hash bound both locks, which is the point, and the preimage now published on the LEZ is what released both.\n\n> **Warning**\n>\n> A Basecamp app can't open your browser for you. Logos app interfaces run inside a sandboxed QML engine that silently ignores requests to open an external URL, as reported in [eth-lez-atomic-swaps#84](https://github.com/logos-co/eth-lez-atomic-swaps/issues/84). That's why every row on the receipt carries copy buttons instead of clickable links—**⧉** for the value itself, **↗** for a block-explorer link—and why every instruction here says to paste the link into your browser yourself.\n\nTwo rows are deliberately copy-only, with no explorer link. **ETH swap ID (lock)** is an identifier internal to the contract rather than a transaction hash, and **LEZ HTLC program** names a [program](https://docs.logos.co/get-started/glossary.md#program) rather than a transaction. Keep the swap ID anyway. It's what the **Refund** tab asks for if you ever have to claim back a swap this install has no record of.\n\n## Step 6: Send feedback\n\nThis app is a testnet preview, and the quickest way to improve it is to report what you hit.\n\nOn any receipt, click **Copy feedback link** and paste it into your browser. It opens a pre-structured feedback form on the app repository. Then click **Copy safe evidence** and paste that into the form. It gathers the chain ID, the transaction hashes, and the explorer links needed to trace your swap on both chains, and it deliberately leaves out your keys, your preimage, the hashlock, and your addresses.\n\nIf you never got as far as a receipt, [open an issue](https://github.com/logos-co/eth-lez-atomic-swaps/issues/new) describing what you expected and what happened instead, with your platform and the `swap` and `swap_ui` versions from the **Package Manager**.\n\n## Troubleshooting\n\n### The app doesn't appear after installing it\n\nRestart Basecamp. Current Basecamp releases add a newly installed app to the sidebar straight away, but older ones only do so after a restart. If it's still missing, check your platform: the catalogue publishes `darwin-arm64`, `linux-amd64`, and `linux-arm64` builds only, and Basecamp installs nothing at all on a platform with no matching build, which is what happens on Intel macOS.\n\n### The Market tab is empty\n\nRead the empty state, because it names the cause. `Finish network setup to browse` means your configuration isn't valid yet, so the app never subscribed to anything. `Connecting to the swap network…` means the app hasn't found a delivery peer yet, which usually resolves on its own within a minute. `No offers on the board yet` means you're connected and configured, and the maker is simply offline for the moment. `Connected, but nobody else is` means the opposite problem: you're on the swap network but attached to no peers, so no offer can reach you—it usually clears on its own, and if it doesn't, check for a module update in Basecamp and restart it. Wait a few minutes and let the board rescan. Because nothing retains offers, the tab can only ever show what's being broadcast right now.\n\n### The app can't connect to Ethereum\n\nCheck **RPC URL** begins with `wss://` and not `https://`. The app opens a WebSocket subscription to watch for lock and claim events, and an `https://` endpoint fails at that point even though it's perfectly valid for ordinary calls. The field's own hint says the same thing.\n\n### The swap does nothing and no error appears\n\nYour LEZ account almost certainly isn't activated. The sequencer discards transactions for an account it has never seen initialised and returns no error, so the app has nothing to report. Open the **Setup** tab from [Step 2](#step-2-set-up-your-accounts) and press the button under **3. Activate your LEZ account** again. It re-checks the registration against the network, and confirms in about a second if the account was already fine.\n\n### The swap stalls on `Wait for the seller`\n\nThe maker refuses a lock that doesn't leave it enough time to respond, and the Ethereum contract enforces its own floor of 300 seconds. Your Ethereum deadline has to sit comfortably beyond the LEZ one, not just after it. This shows up when a swap is started against an offer that's nearly expired, so take a freshly published one. The maker keeps waiting rather than reporting a failure, so your **Swap** tab appears to stall instead of showing an error.\n\n### There aren't enough LEZ tokens in the account\n\nCheck first that this is really your problem. Taking a buy offer requires no LEZ tokens. If you're publishing sell offers instead, buy LEZ tokens on the **Market** tab first, as described in [Get LEZ tokens to sell](#get-lez-tokens-to-sell).\n\n### Setup shows a `Fund LEZ` step instead\n\nYou're on an older build, or on one launched with the developer override that restores the previous screens. Up to `swap_ui` `0.4.5`, step 3 both activated and funded the account, and **Start trading** was numbered as a fifth step. That funding no longer works on the testnet. Update `swap` and `swap_ui` from the **Package Manager** to get the four-step flow this page describes.\n\n### A swap stopped halfway and the funds are still locked\n\nThis is the case the time locks exist for, and no funds are at risk. Open the **Refund** tab: a **Ready to claim back** card carries the **Hashlock** and **Swap ID** from your most recent swap, already filled in, so click **Get my ETH back** once your deadline has passed. For an older swap, expand **Enter a refund by hand (advanced)**, copy **ETH swap ID (lock)** from its receipt in the **History** tab, and paste it into **Swap ID** under **Get ETH back**. The contract enforces the deadline, so an early attempt fails outright rather than passing silently. Reclaiming returns your ETH in full, less gas.\n\n## Next steps\n\n- [Install and load a module in Basecamp](https://docs.logos.co/basecamp/install-and-load-module-in-basecamp.md) explains how Basecamp packages, installs, and loads the [modules](https://docs.logos.co/get-started/glossary.md#module) this app is built from.\n- [Run the LEZ wallet app and initiate native token transfers](https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md) covers moving the LEZ tokens you just received.\n\nTo build the app from source, or run a swap headlessly from the command line, see the [eth-lez-atomic-swaps README](https://github.com/logos-co/eth-lez-atomic-swaps/blob/master/README.md).\n"},{"id":"blockchain/introduction-to-the-logos-blockchain","title":"Introduction to the Logos Blockchain","summary":"Learn how Logos provides a private, resilient foundation for building decentralised applications.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain","markdown_url":"https://docs.logos.co/blockchain.md","permalink":"/blockchain","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/introduction-to-the-logos-blockchain.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Architecture"},{"depth":3,"text":"Bedrock"},{"depth":3,"text":"Blend Network"},{"depth":3,"text":"Zones"}],"markdown":"# Introduction to the Logos Blockchain\n\n#### Learn how Logos provides a private, resilient foundation for building decentralised applications.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nThe [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) is the foundational infrastructure of the Logos technology stack. It gives Logos applications a [common context for interaction](#bedrock), and allows them to leverage the consensus guarantees provided by the entire network. Nodes can participate with consumer hardware and any amount of stake.\n\n## The basics\n\n- The Logos Blockchain is used for building decentralised applications.\n- Nodes can join with typical consumer hardware and any amount of stake, which keeps participation barriers low and improves decentralisation.\n- Logos Blockchain consists of two layers: lightweight, permissionless blockchains called [Zones](https://docs.logos.co/blockchain/concepts/about-zones.md) built on top of a Layer 1 foundation called [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md). The [Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md) is used to improve [proposer privacy](https://docs.logos.co/get-started/glossary.md#proposer-privacy).\n\n## Architecture\n\nThe Logos Blockchain is implemented as two blockchain layers. Application execution happens on lightweight, permissionless blockchains called [Zones](https://docs.logos.co/blockchain/concepts/about-zones.md), which are built on a Layer 1 foundation called [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md). The [Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md) Service extends Bedrock's privacy properties.\n\n### Bedrock\n\nBedrock is a large-scale validator network that serves as the foundational layer of the Logos Blockchain. It provides consensus, data availability, and lightweight verification to Zones. Its [Private Proof of Stake](https://docs.logos.co/get-started/glossary.md#private-proof-of-stake) ([PPoS](https://docs.logos.co/get-started/glossary.md#ppos)) consensus protocol, [Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md), improves proposer privacy while staying scalable, resilient, and accessible. Bedrock also enables decentralised sequencing for Zones, note [mining](https://docs.logos.co/get-started/glossary.md#mining), token bridging, and inter-Zone messaging.\n\nRunning a Bedrock validator node means leaving the [Logos node](https://docs.logos.co/get-started/glossary.md#logos-node) application running in the background, with a low-maintenance approach. This makes it straightforward to contribute to the security, consensus, and interoperability of the network.\n\n### Blend Network\n\nThe Blend Network is a Blockchain [Service](https://docs.logos.co/get-started/glossary.md#service), through which declared Blend nodes provide network-level privacy for block proposers participating in consensus. It virtually eliminates the chance of linking a block proposer or transaction sender to their block or transaction, ensuring that they cannot be targeted. Node operators who join the Blend Network need more hardware than Bedrock validation alone requires.\n\n### Zones\n\nApplications do not run directly on Bedrock. They run on Layer 2 blockchains called [Zones](https://docs.logos.co/blockchain/concepts/about-zones.md), which define their own state transitions and validity but rely on Bedrock for consensus guarantees. This modular design lets creators customise a chain and maximise properties such as performance.\n\nZones can run as fully independent sovereign rollups, but Bedrock also enables token bridging and decentralised sequencing. Zones can exchange asynchronous [messages](https://docs.logos.co/get-started/glossary.md#message) through Bedrock, which allows limited interoperability without giving up the advantages of the sovereign rollup model.\n\nThe primary example is the [Logos Execution Zone](https://docs.logos.co/lez.md), the initial Zone for the messaging and storage applications of the Logos stack, with support for private token transfers and [program](https://docs.logos.co/get-started/glossary.md#program) execution.\n"},{"id":"blockchain/get-started/run-blockchain-node-from-basecamp","title":"Run a Blockchain node from Basecamp","summary":"Install the blockchain modules and run a syncing testnet node from the Basecamp desktop app.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp","markdown_url":"https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md","permalink":"/blockchain/get-started/run-blockchain-node-from-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/get-started/run-blockchain-node-from-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install and launch the blockchain app"},{"depth":2,"text":"Step 2: Start the Node"},{"depth":3,"text":"Quick start"},{"depth":3,"text":"Step-by-step-setup"},{"depth":2,"text":"Step 3: Verify that the node is syncing"},{"depth":2,"text":"Step 4: Fund your node and propose blocks"},{"depth":2,"text":"Step 6: Keeping your node running"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"The welcome screen only offers Set up your node"},{"depth":3,"text":"A dialogue says `Your config is out of date`"},{"depth":3,"text":"The headline stays in Bootstrapping mode"},{"depth":3,"text":"The headline reads Node stopped or Disconnected"}],"markdown":"# Run a Blockchain node from Basecamp\n\n#### Install the blockchain modules and run a syncing testnet node from the Basecamp desktop app.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThe [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) is the blockchain [module](https://docs.logos.co/get-started/glossary.md#module) of the Logos technology stack. You can run a node [from the CLI](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) or from [**Basecamp**](https://docs.logos.co/get-started/glossary.md#basecamp), the local-first launcher for peer-to-peer apps built on Logos, by installing the blockchain module and its UI.\n\nThis procedure covers running the Logos Blockchain app via Basecamp, setting up a node through the onboarding wizard, confirming that it syncs, and funding its wallet so it can propose blocks.\n\n> **Info: Prerequisites**\n>\n>\n> - [Basecamp installed and running](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - A graphical desktop session on one of:\n>    - **Linux** (tested on Ubuntu 24.04)\n>    - **Windows 11 with WSL2** (plus WSLg for the GUI)\n>    - **Apple Silicon Mac** (M1 or later).\n> - CPU: 2 Cores, 2Ghz. Modern multi-core processor. **Must have ADX instruction support (on x86_64)**, such as Intel Broadwell or later, or any AMD Zen. On virtual machines, configure the hypervisor to pass through host CPU features. Generic CPU models such as `kvm64` and `qemu64` hide ADX and cause the blockchain module to crash with `signal 4`.\n> - Memory (RAM): Minimal (1 Gb).\n> - Storage:\n>    - For joining the testnet or a short-lived node: SSD with 1-2 GB free.\n>    - Expected for a long-lived mainnet node: SSD with 100+ GB free with ability to expand storage on demand.\n> - Network: Relatively reliable network connection. 1Mbps of free bandwidth.\n\nPort forwarding is **not** required. A blockchain node participates outbound-only: it syncs, validates, and (once funded) proposes blocks. Forwarding only makes your node *reachable* so others can use it, which is out of scope here.\n\n## What to expect\n\nBy the end of this procedure:\n\n- Basecamp will be running the Logos Blockchain [app](https://docs.logos.co/get-started/glossary.md#app).\n- Your node will be connected to testnet peers and syncing the chain.\n- You will confirm sync by observing an advancing block height.\n- Optionally, you can fund your wallet by mining so your node proposes blocks.\n\n## Step 1: Install and launch the blockchain app\n\n1. In Basecamp, press **Discover Applications** on the home screen or the top button of the group at the bottom of the sidebar on the left to open the **Applications** section.\n\n   ![Basecamp landing](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/basecamp-landing.png)\n\n1. In the **Blockchain** category, select **Blockchain**.\n\n   ![Blockchain node application](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/blockchain-app.png)\n\n1. Make sure that `Blockchain` (the UI module) is set to `v0.3.2` and `Blockchain Module` (the core module) is set to `v0.3.2`, then click **Install**.\n\n   ![Install the Blockchain app](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/blockchain-app-install.png)\n\n   > **Note**\n   >\n   > Individual [package](https://docs.logos.co/get-started/glossary.md#package) versions are pinned independently and do not necessarily match the testnet version number.\n\n1. Once the modules are installed, press **Launch**.\n\n   ![Launch the Blockchain app](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/blockchain-app-launch.png)\n\n   - The Blockchain app will open.\n\n## Step 2: Start the Node\n\nOn first run, the app opens on a welcome screen titled **Blockchain Node**. A node needs a config and a keystore before it can start.\n\nThe welcome screen offers **Quick start** and **Advanced**. [**Quick start**](#quick-start) generates a config for the public testnet with the bootstrap peers built into the release, creates your keys, and starts the node. To walk through the same choices one at a time, or to reuse a config you already have, click **Advanced** and follow [Step-by-step setup](#step-by-step-setup) instead.\n\n   ![Blockchain app landing page](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/blockchain-landing.png)\n\n### Quick start\n\n1.  Click **Quick start**.\n\n   - The screen reads **Setting up your node…** while the app writes your config, creates your keys, and starts the node. The node view then opens.\n\n1.  Open the **Settings** tab. Until you back up your keys, the **Node** tab shows a **Back up your keys** notice that points here.\n\n1.  In the **Back up your keys** card, click **Download keystore.yaml** and choose where to save the copy.\n\n   ![The Back up your keys card in Settings](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/settings-backup-keys.png)\n\n   - A **Keystore saved** message confirms the backup, and the card shows a **Done** badge.\n\n   > **Warning**\n   >\n   > Your keystore holds the keys to your accounts, stake, and rewards. Nothing can reissue them. Keep the copy somewhere other than this machine.\n\n### Step-by-step-setup\n\n1. Click **Advanced**. The **Set up your node** wizard opens on step **1. Setup**.\n\n1. Under **How do you want to start?**, select **Generate a new node** and click **Continue**.\n\n   ![Set up your node](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/advanced-setup.png)\n\n   - To reuse files you already have, select **Use an existing config** instead, fill in **User config** (and optionally **Deployment**) under **Point to your files**, and click **Start node**. The wizard ends there.\n\n1. On **2. Network**, leave **Default (public testnet)** selected. The **Bootstrap peers** field is already filled with the testnet peers built into the release. A custom deployment file or different bootstrap peers can also be provided via this page. Click **Generate config** to proceed.\n\n   ![Set up your node's network settings](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/advanced-network.png)\n\n   > **Caution**\n   >\n   > A config generated with an empty **Bootstrap peers** field produces a node that starts and reports success but never syncs. Make sure the peers above are present before generating.\n\n1. On **3. Keys**, click **Download keystore.yaml** and save the copy somewhere safe. Select **I've backed up my keys somewhere safe**, then click **Continue**.\n\n   ![Back up your node's keys](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/advanced-keys.png)\n\n1. To participate in consensus, your node needs some testnet notes. Notes can be obtained by engaging in PoW mining. On **4. Fund**, choose whether the node claims mined rewards automatically:\n\n   - Leave **Claim mined rewards automatically** on. With this approach, your node will repeatedly claim more notes until the target accounts reach their cap. **Pays into** already lists the target from your generated config: your **PoWClaim** account, with **No cap**. To pay into another account as well, choose it, set a threshold or switch on **No cap**, and click **Add**.\n   - Alternatively, switch it off. You can still mine with the **Fund** button and claim rewards yourself from the **Mining** tab.\n\n   ![Choose a way to fund your node](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/advanced-fund.png)\n\n   > **Tip**\n   >\n   > Keeping all the defaults on this page is the recommended approach.\n\n1. Click **Start node**. The app saves your mining settings, starts the node, and opens the node view.\n\n## Step 3: Verify that the node is syncing\n\nThe **Node** tab shows the node's consensus status as a headline, with a lifecycle lane underneath it (**Started**, **Online**, **Funded**, **Aged**, **Proposing**, **Earning**) that lights up one stage at a time.\n\n1.  Watch the headline. It moves from **Starting** to **[Bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping)** (**Syncing with the chain**) while the node catches up, then to **Online** (**Following the chain**).\n\n   ![Bootstrapping node](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/bootstrapping-node.png)\n\n   > **Note**\n   >\n   > If you were offline for a while, expect the node to sit in *Bootstrapping* while it catches up before it reports *Online*. A height that is far below the current [slot](https://docs.logos.co/get-started/glossary.md#slot) during initial sync is normal.\n\n1.  Check the **Peers** tile. It shows the number of connected peers, and should be greater than `0`.\n\n1.  Check the **Height** tile. Expect it to increase by about one block every 10 seconds once the node is **Online**. The timing is probabilistic, so some variance is normal.\n\n   - You can compare the height with the [testnet block explorer](https://testnet.blockchain.logos.co/web/explorer/).\n   - The **Explorer** tab lists each new block that this node sees from now on.\n\n## Step 4: Fund your node and propose blocks\n\nA synced node validates the chain but does not propose blocks until its wallet holds testnet notes. Notes are obtained by PoW mining. \n\n1. Wait until the node reaches **Online** mode. The **Fund** button at the top right stays disabled until then.\n\n   ![Online node](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/online-node.png)\n\n1. Click **Fund**. The node starts mining in the background and the button changes to **Stop Mining**.\n\n   > **Warning**\n   >\n   > Mining uses CPU for as long as it runs. It does not stop by itself: click **Stop Mining** when you have enough funds.\n\n1. Open the **Mining** tab to follow mined tickets. A ticket pays nothing until it is claimed, and expires if it never is claimed.\n\n   - By default, the node claims tickets for you and pays them into your **PoWClaim** account, whether you used **Quick start** or step-by-step setup: the generated config lists that account as an auto-claim target.\n   - If you switched auto-claim off during [step-by-step setup](#step-by-step-setup), claim tickets yourself under **Manual claim**.\n   - The **Mining Rewards** tile on the **Node** tab shows how much has been claimed so far.\n\n   ![The header showing Stop Mining while the node mines](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/node-mining.png)\n\n1. Open the **Wallet** tab, then click **Accounts**. Click **Refresh** and confirm the balance of your **PoWClaim** account, in TestToken.\n\n   ![Wallet > Accounts with a funded PoWClaim account](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/wallet-accounts.png)\n\n1. Once the wallet is funded, the **Stake** tile on the **Node** tab shows the value of the notes that have aged into the leadership lottery. Tokens can take up to two [epochs](https://docs.logos.co/get-started/glossary.md#epoch) (3.5 hours) to become eligible for consensus. After that, the node takes part in the consensus lottery automatically and starts proposing blocks. Confirm that your node is participating by checking that the headline has reached **Aged** and **Proposing**\n   \n   > **Info**\n   >\n   > Block proposal is probabilistic. Your node will not propose on every slot; participation depends on your stake relative to total active stake in the network.\n\n1. Rewards appear under the **Rewards**, where **Claim** redeems them into your wallet.\n\n   ![Leader rewards](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/run-a-logos-blockchain-node-from-basecamp/leader-rewards.png)\n\n   > **Note**\n   >\n   > Participating in the [Blend network](https://docs.logos.co/get-started/glossary.md#blend-network) as a [*Core* node](https://docs.logos.co/get-started/glossary.md#core-node) requires a separate locked [note](https://docs.logos.co/get-started/glossary.md#note) (staked funds) and does not affect block proposing. See [Join the Blend network as a Core node](https://docs.logos.co/blockchain/blend/join-the-blend-network-as-a-core-node.md).\n\n## Step 6: Keeping your node running\n\nThe node runs only while Basecamp is open, and it does not resume automatically after a restart. To restart your node, reopen Basecamp, open the blockchain module, and select **Start Node** again. For an unattended, always-on node, use the [CLI setup](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) with a service manager instead.\n\n## Troubleshooting\n\n### The welcome screen only offers Set up your node\n\nThe app hides **Quick start** when it has no bootstrap peers: either it could not read the peers built into its `metadata.json`, or it was built from `master`, which has none. Click **Set up your node** and follow [Step-by-step setup](#step-by-step-setup). On **2. Network**, the **Bootstrap peers** field will be empty: copy the testnet peers from the `Initialize Your Node` section of the [Logos Blockchain release notes](https://github.com/logos-blockchain/logos-blockchain/releases/latest) and paste them one per line. The release notes show them inside a CLI command: keep only the multiaddrs themselves, such as `/ip4/<ip>/udp/<port>/quic-v1/p2p/<peer-id>`, with no quotes, brackets, or commas.\n\n### A dialogue says `Your config is out of date`\n\nThe config was written for an older release, so the node won't start with it. Testnet v0.3 starts a new chain, so configs from Testnet v0.2 need updating. Click **Update config**: the app rebuilds the config for this release and keeps your settings and keys. If the dialogue reads `This config can't be updated` instead, there is no keystore next to the config, and **Start fresh** creates a new config with a new set of keys.\n\n### The headline stays in Bootstrapping mode\n\nThe node is still catching up, and the line beside the headline says why:\n\n- `Catching up — replaying stored blocks.` or `Syncing with the chain`: wait. A first sync can take a while.\n- `No block progress for 10 minutes — the node may have lost its peers. Try stopping and starting it.`: click **Stop Node**, then **Start Node**.\n- `Genesis is … — the node can't finish syncing until then. Its config likely points at the wrong network.`: the config was generated for another network. In **Settings**, click **Start a new node** to generate a config for the public testnet.\n\nIf the node is stuck or will not start at all, the **Reset the database** card in **Settings** shows the database folder. Stop the node, delete that folder, then start the node again. It re-syncs the chain from its peers, and your keys and config files are untouched.\n\n### The headline reads Node stopped or Disconnected\n\n- **Node stopped** means the node process stopped unexpectedly. Click **Start Node**. If the same thing happens again, restart the app.\n- **Disconnected** means the app lost contact with the node service. Restart the app.\n"},{"id":"blockchain/get-started/run-blockchain-node-from-cli","title":"Run a Blockchain node from the CLI","summary":"Start a node on the public testnet and verify runtime and consensus signals.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli","markdown_url":"https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md","permalink":"/blockchain/get-started/run-blockchain-node-from-cli","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/get-started/run-blockchain-node-from-cli.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Load the Logos Blockchain module"},{"depth":2,"text":"Step 2: Configure and start the node"},{"depth":2,"text":"Step 3: Verify that your node is running and connected to peers"},{"depth":2,"text":"Step 4: Mine tokens to fund your node"},{"depth":2,"text":"Step 5: Claim leader rewards"},{"depth":2,"text":"Troubleshooting the Logos Blockchain node"},{"depth":3,"text":"`logosctl call` fails with `RPC call failed`?"},{"depth":3,"text":"My node stays in `Bootstrapping` for hours and its height stops increasing?"},{"depth":3,"text":"How do I look up a transaction on the testnet explorer?"},{"depth":3,"text":"My wallet balance is not updating after mining?"}],"markdown":"# Run a Blockchain node from the CLI\n\n#### Start a node on the public testnet and verify runtime and consensus signals.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nWith this tutorial, you will install the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) node, connect to the public testnet, and verify that your node is running. The Logos Blockchain is the blockchain component of the Logos technology stack, providing a privacy-preserving and censorship-resistant framework for decentralised applications. This procedure is for node operators setting up a node for the first time.\n\n> **Info: Prerequisites**\n>\n>\n> - CPU: 2 Cores, 2Ghz. Modern multi-core processor. **Must have ADX instruction support (on x86_64)**, such as Intel Broadwell or later, or any AMD Zen. On virtual machines, configure the hypervisor to pass through host CPU features. Generic CPU models such as `kvm64` and `qemu64` hide ADX and cause the blockchain module to crash with `signal 4`.\n>     - Check whether your CPU has ADX support by running:\n>\n>     ```sh\n>     # Linux\n>     grep -o adx /proc/cpuinfo | head -1\n>\n>     # macOS\n>     sysctl -a | grep -i adx\n>     ```\n> - A supported OS:\n>     - Linux x86_64\n>     - macOS aarch64 (recent versions)\n>     - Raspberry Pi 5 with [Raspberry Pi OS](https://www.raspberrypi.com/software/)\n> - glibc version 2.39 or later (Linux only)\n> - FUSE, required by the `logosctl` AppImage (Linux only). On Debian/Ubuntu: `sudo apt install fuse libfuse2t64`. Without it, `logosctl` fails with `No suitable fusermount binary found on the $PATH`.\n> - Memory (RAM): Minimal (1 Gb).\n> - Storage:\n>    - For joining the testnet or a short-lived node: SSD with 1-2 GB free.\n>    - Expected for a long-lived mainnet node: SSD with 100+ GB free with ability to expand storage on demand.\n> - Network: Relatively reliable network connection. 1Mbps of free bandwidth.\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n\n## What to expect\n\n- You can install the node binary, generate a configuration, and join the public testnet.\n- You can verify that your node is syncing and connected to peers using the local API.\n- You can mine test tokens and automatically participate in the consensus lottery once your stake matures.\n\n## Step 1: Load the Logos Blockchain module\n\nDownload the Logos Blockchain [module](https://docs.logos.co/get-started/glossary.md#module) from the [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue), then load it in `logosctl`.\n\n1.  Start `logosctl` from the directory where you want the node's files to live, for example your home directory:\n\n    ```sh\n    cd ~\n    logosctl daemon start\n    ```\n\n    - The blockchain module writes `user_config.yaml`, `keystore.yaml`, the `state/` directory and the node log files to the daemon's working directory, and resolves relative config paths against it.\n\n1.  In a new terminal window with the same user, refresh the official module catalogue. Run this even if the daemon was already running, because it does not see packages published after it last refreshed:\n\n    ```sh\n    logosctl catalog refresh\n    ```\n\n1.  Install the Logos Blockchain module [package](https://docs.logos.co/get-started/glossary.md#package) version 0.3.2:\n\n    ```sh\n    logosctl package install blockchain_module \\\n    --version 0.3.2 \\\n    --yes\n    ```\n\n    > **Note**\n    >\n    > Individual module package versions are pinned independently and do not necessarily match the testnet version number.\n\n1.  Load the Logos Blockchain module:\n\n    ```bash\n    logosctl module load blockchain_module\n    ```\n\n    - A `module load` sent before the daemon is ready fails with an RPC or missing client config error. If that happens, check `logosctl status` again and retry.\n\n## Step 2: Configure and start the node\n\nThe `generate_user_config` subcommand generates a user configuration that includes per-node settings such as keys, ports, and peer addresses, along with fresh cryptographic keys and an auto-detected public IP.\n\n> **Info**\n>\n> Make sure to use the current bootstrap peer addresses in the [Logos Blockchain Node release notes](https://github.com/logos-blockchain/logos-blockchain/releases/latest) for your selected release.\n\n1.  Generate your `user_config.yaml` and `keystore.yaml` files by running `generate_user_config` with the bootstrap peer addresses. The daemon writes both files to the directory you ran `logosctl daemon start` from, so run the commands in this step from that directory. The examples assume it is `$HOME`. For example, for release 0.3.2:\n\n    ```sh\n    logosctl call blockchain_module generate_user_config '{\n        \"initial_peers\": [\n            \"/ip4/65.109.51.37/udp/3000/quic-v1/p2p/12D3KooWFrouXfmrR4nsLMtE7wu15DoMJ6VtoUtHinREZCvbWHar\",\n            \"/ip4/65.109.51.37/udp/3001/quic-v1/p2p/12D3KooWJRGau8M1rjT7R5e4YYsgdFhsMX35nRDtMwCDjxQkXAHz\",\n            \"/ip4/65.109.51.37/udp/3002/quic-v1/p2p/12D3KooWQXJavMDTRscjauFSgVAB1VLB6Rzpy2uY5SU9Tk7927tb\",\n            \"/ip4/65.109.51.37/udp/50001/quic-v1/p2p/12D3KooWSQc7CcGtvWDPF1yCbBthFnQjprfCVHmfmNDUrSmqQsU1\"\n        ]\n    }'\n\n    chmod 600 user_config.yaml keystore.yaml\n    ```\n\n    > **Info**\n    >\n    > `user_config.yaml` contains node-local wallet and key-management configuration. Keep it private, restrict file permissions, and do not publish it. Generate a fresh file for each node.\n\n    - Important fields in `user_config.yaml` include:\n\n    | Field | Purpose | Guidance |\n    |-------|---------|----------|\n    | `network.backend.initial_peers` | Bootstrap peers | Use the current network document |\n    | `network.backend.swarm.port` | Public UDP P2P port | Keep aligned with firewall/NAT, normally `3000` |\n    | `api.backend.listen_address` | Local API bind | Keep private, normally `127.0.0.1:8080`. Edit the file if you want to change the port |\n    | `state.base_folder` | State directory | Use a persistent local path |\n    | logger filters | Log verbosity | Use `INFO` for unattended operation |\n\n1.  Start the node:\n\n    ```sh\n    logosctl call blockchain_module start \"$HOME/user_config.yaml\" \"\"\n    ```\n\n    - A relative path is resolved against the daemon's working directory. If you started the daemon elsewhere, pass the absolute path to `user_config.yaml`.\n    - The second argument is intentionally an empty string; the blockchain module no longer requires a downloaded `deployment.yaml` file.\n\n    > **Info**\n    >\n    > The Logos Blockchain node does not currently support dynamic wallet key management. To add new keys you must manually edit `user_config.yaml` and restart the node. If the node is restarted while [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping), it does not save sync progress and restarts from the beginning.\n\n## Step 3: Verify that your node is running and connected to peers\n\nWait for your node to finish syncing and reach `Online` mode before mining. Pipe the `get_cryptarchia_info` command through `jq .` to format JSON output.\n\n1.  Check the consensus state. The `logosctl` call and the node's HTTP endpoint return the same data in slightly different shapes.\n\n    ```sh\n    logosctl call blockchain_module get_cryptarchia_info | jq -r .result.value | jq .\n    ```\n\n    Example response (the `logosctl` call returns a flat object with a `mode` field):\n\n    ```json\n    {\n      \"lib\": \"3d0c...4e6d\",\n      \"lib_slot\": 0,\n      \"tip\": \"f44d...e2f5\",\n      \"slot\": 70899,\n      \"height\": 120,\n      \"mode\": \"Bootstrapping\"\n    }\n    ```\n\n    Alternatively, send a request directly to your node port:\n\n    ```sh\n    curl -s http://localhost:8080/cryptarchia/info | jq .\n    ```\n\n    Example response (the HTTP endpoint nests the fields under `cryptarchia_info`, names the status field `state`, and adds a top-level `phase`):\n\n    ```json\n    {\n      \"cryptarchia_info\": {\n        \"lib\": \"3d0c...4e6d\",\n        \"lib_slot\": 0,\n        \"tip\": \"f44d...e2f5\",\n        \"slot\": 70899,\n        \"height\": 120,\n        \"state\": \"Bootstrapping\"\n      },\n      \"phase\": \"ProlongedBootstrapPeriod\"\n    }\n    ```\n\n    - The status field (`mode` from the `logosctl` call, `state` from the HTTP endpoint) starts as `Bootstrapping` while syncing and transitions to `Online` once caught up.\n    - Confirm `slot` and `height` are increasing. `height` counts confirmed blocks; `slot` counts elapsed time intervals, with a new block expected roughly every 10 seconds.\n\n1.  Check peer connectivity:\n\n    ```sh\n    curl -s http://localhost:8080/network/info | jq .\n    ```\n\n    Example response:\n\n    ```json\n    {\n      \"listen_addresses\": [\"/ip4/127.0.0.1/udp/3001/quic-v1\"],\n      \"peer_id\": \"12D3...fuS2\",\n      \"connected_peers\": [\"12D3...Mxu1\", \"12D3...sbD3\"],\n      \"discovered_peers\": [\"12D3...Mxu1\", \"12D3...sbD3\"],\n      \"n_peers\": 16,\n      \"n_connections\": 19,\n      \"n_discovered_peers\": 18,\n      \"n_pending_connections\": 0\n    }\n    ```\n\n    - Confirm `n_peers` is greater than `0`.\n\n1. After 30–60 seconds, run the `get_cryptarchia_info` command again and confirm `slot` and `height` have increased.\n\n1. Wait until `mode` transitions to `Online` before continuing. Bootstrapping should take approximately 1 hour.\n\n## Step 4: Mine tokens to fund your node\n\nA synced node validates the chain but does not propose blocks unless its wallet holds notes, which can be obtained by mining. The `user_config.yaml` generated in [Step 2](#step-2-configure-and-start-the-node) includes a `pow` section that automatically claims mined rewards into your node's `PoWClaim` key.\n\n> **Warning**\n>\n> Do not call `pow_status` or `pow_claimable_rewards` before the node is `Online`. While the node is bootstrapping these calls never return, and every later `logosctl call blockchain_module` command fails with `RPC call failed` until you restart the daemon with `logosctl daemon stop`.\n\n1.  After your node reaches `Online` mode, start mining:\n\n    ```sh\n    logosctl call blockchain_module pow_start_mining\n    ```\n\n    - Mining is off by default and does not persist across restarts. Run `pow_start_mining` again after every restart.\n    - Auto-claim starts automatically. You do not need to call `pow_start_auto_claim`.\n\n1.  Check the mining and auto-claim status:\n\n    ```sh\n    logosctl call blockchain_module pow_status | jq -r .result.value | jq .\n    ```\n\n    - `is_mining` is `true`, and the `balance` of the auto-claim target grows as auto-claim collects rewards. The target's `public_key` is your `PoWClaim` key.\n\n1.  Check the balance of your `PoWClaim` key. Replace `<your-powclaim-key>` with the `public_key` from the previous step:\n\n    ```sh\n    curl -s http://localhost:8080/wallet/<your-powclaim-key>/balance | jq .\n    ```\n\n1.  Mining uses CPU for as long as it runs. Once you have enough funds, stop it:\n\n    ```sh\n    logosctl call blockchain_module pow_stop_mining\n    ```\n\n> **Info**\n>\n> Your tokens become eligible for consensus after 3.5 hours. Confirm that your node is participating by checking that `mode` remains `Online` and `height` continues to increase.\n>\n> Block proposal is probabilistic. Your node will not propose on every [slot](https://docs.logos.co/get-started/glossary.md#slot); participation depends on your stake relative to total active stake in the network.\n\n## Step 5: Claim leader rewards\n\nEach block your node proposes mints a **leader-reward voucher**. On a CLI node, vouchers are not credited automatically; you claim them to turn them into spendable balance.\n\nRun this step only once your node reports `mode: Online` ([Step 3](#step-3-verify-that-your-node-is-running-and-connected-to-peers)) and your stake has matured. The chain-leader service is not available while the node is still `Bootstrapping`, and `leader_claim` fails there with an unrelated error rather than the response shown below.\n\n1.  Check what is currently claimable. The `value` field is a JSON string, so extract and re-parse it:\n\n    ```sh\n    logosctl call blockchain_module wallet_get_claimable_vouchers | jq -r .result.value | jq .\n    ```\n\n    Example response:\n\n    ```json\n    {\n      \"tip\": \"e5f28df8...9153e\",\n      \"vouchers\": []\n    }\n    ```\n\n1.  Claim an available voucher:\n\n    ```sh\n    logosctl call blockchain_module leader_claim\n    ```\n\n    On success the call returns `success: true` with the claim transaction hash in `value`. When there is nothing to claim, it returns:\n\n    ```json\n    {\n      \"method\": \"leader_claim\",\n      \"module\": \"blockchain_module\",\n      \"result\": {\n        \"error\": \"Failed to claim leader rewards: Chain leader service error: Wallet API error: No claimable voucher found\",\n        \"success\": false,\n        \"value\": null\n      },\n      \"status\": \"ok\"\n    }\n    ```\n\n> **Info**\n>\n> `leader_claim` claims **one** voucher per call. To drain several, check `wallet_get_claimable_vouchers` and call `leader_claim` once per listed voucher, pausing a few seconds between calls to avoid a burst of failures.\n>\n> A voucher becomes claimable only once it is provable against the current tip, so a claim can be a temporary no-op (`No claimable voucher found`) shortly after a block is led; retry on the next check. `leader_claim` currently returns only the transaction hash, not the voucher it consumed or the fee paid.\n>\n> Distinguish the two failure shapes. A reply with `\"status\": \"ok\"` and `result.success: false` means the node answered and had nothing to claim. A reply with `\"status\": \"error\"` and no `result` object means the call never reached the chain-leader service: either it timed out (`call to 'blockchain_module.leader_claim' timed out after 20000ms`) or the service is not running yet (`Failed to establish connection to chain-leader-service`). Both of the latter mean the node is not `Online` yet; re-check [Step 3](#step-3-verify-that-your-node-is-running-and-connected-to-peers) rather than retrying the claim.\n\n## Troubleshooting the Logos Blockchain node\n\n### `logosctl call` fails with `RPC call failed`?\n\nAn error such as:\n\n```\n{\"code\":\"RPC_FAILED\",\"message\":\"callModuleMethod('blockchain_module','generate_user_config') RPC call failed.\",\"status\":\"error\"}\n```\n\nmeans the `logosctl` daemon isn't reachable, or the module isn't loaded. Run `logosctl daemon status` to tell the cases apart: it reports the daemon state, `running` or `not_running`, and the status of each module, `loaded`, `not_loaded`, or `crashed`. Restart the daemon if needed, then load the module:\n\n```bash\nlogosctl daemon start --detach\nlogosctl module load blockchain_module\n\nlogosctl daemon status\n```\n\nIf the module shows `not_loaded` again after a successful `load-module`, or calls keep failing, check the daemon output for a module crash:\n\n```\n[critical] [logos] [blockchain_module] FATAL: module 'blockchain_module' crashed (signal 4).\n```\n\n`signal 4` is an illegal-instruction fault. One known cause: the blockchain module requires a CPU with ADX support. Run `grep -c adx /proc/cpuinfo` to check. An output of `0` means the CPU, or the CPU model of the VM, lacks ADX. On physical hardware, the module needs an Intel Broadwell or later, or an AMD Zen CPU. On a virtual machine, set the CPU model to pass through host features, for example `host` in QEMU and Proxmox.\n\nIf the count is greater than `0`, the crash has a different cause. Collect the complete `FATAL` lines from the daemon output, including the backtrace addresses, together with the last lines of the newest node log file in the directory where the daemon runs, and report them to the Logos team.\n\nLoaded modules don't persist across daemon restarts, so always re-run `load-module` after restarting the daemon. A `METHOD_FAILED` error such as `Call to blockchain_module.<method> failed.` means the daemon is reachable but the call itself failed. The most common causes are a module that isn't loaded or a missing required argument, such as calling `generate_user_config` without the JSON `initial_peers` argument.\n\n### My node stays in `Bootstrapping` for hours and its height stops increasing?\n\nCheck the node log (the `<number>.latest` file in the directory where you started the daemon). If it repeats `failed to process block; cancelling the download err=BlockProcessing(Cryptarchia(FutureBlock { block_slot: ..., current_slot: ... }))`, the initial block download has stalled. Restart the module; it keeps its state and resumes from the blocks it already stored:\n\n```sh\nlogosctl module unload blockchain_module\nlogosctl module load blockchain_module\nlogosctl call blockchain_module start user_config.yaml \"\"\n```\n\nThe `start` call can return `RPC call failed` while the node replays its stored blocks. Wait a few minutes, then check `get_cryptarchia_info` again: `height` should be increasing towards the network tip.\n\n### How do I look up a transaction on the testnet explorer?\n\nOpen `https://testnet.blockchain.logos.co/web/explorer/transactions/<TX_HASH>`, replacing `<TX_HASH>` with the transaction hash. The [testnet explorer](https://testnet.blockchain.logos.co/web/explorer/) home page has no search box for transaction hashes, and searching by address is not supported.\n\n### My wallet balance is not updating after mining?\n\nIf the balance endpoint returns `404` with `The requested address could not be found in the wallet`, your node hasn't yet synced past the block containing the claim transaction. Funded addresses aren't visible while the node is still `Bootstrapping`. Wait for the node to reach `Online` mode and check again. If `pow_status` shows `is_mining` as `false`, run `pow_start_mining` again: mining stops when the node restarts.\n"},{"id":"blockchain/get-started/claim-leader-rewards-in-basecamp","title":"Claim leader rewards in Basecamp","summary":"Learn how to get rewarded for having your Logos Blockchain node propose blocks.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/get-started/claim-leader-rewards-in-basecamp","markdown_url":"https://docs.logos.co/blockchain/get-started/claim-leader-rewards-in-basecamp.md","permalink":"/blockchain/get-started/claim-leader-rewards-in-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/get-started/claim-leader-rewards-in-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Claim a reward voucher"},{"depth":2,"text":"Claim mining rewards"},{"depth":2,"text":"Troubleshooting leader rewards"},{"depth":3,"text":"Ready to claim shows 0"},{"depth":3,"text":"The Rewards tab shows `Start the node from the Node tab.`"},{"depth":3,"text":"Clicking Claim shows Claim failed"},{"depth":3,"text":"The claim stays in Submitted"},{"depth":3,"text":"The account balance has not changed after claiming"},{"depth":3,"text":"The Mining tab shows `Nothing is claiming these tickets`"}],"markdown":"# Claim leader rewards in Basecamp\n\n#### Learn how to get rewarded for having your Logos Blockchain node propose blocks.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure covers how to claim rewards for participating in the consensus protocol of the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain). Every block your node leads through [Proof of Leadership](https://docs.logos.co/get-started/glossary.md#proof-of-leadership) mints a reward voucher, and claiming a voucher redeems it into spendable balance. It is intended for node operators running their node via the Logos Blockchain [app](https://docs.logos.co/get-started/glossary.md#app) on Basecamp. The procedure also covers claiming the proof-of-work tickets that [mining](https://docs.logos.co/get-started/glossary.md#mining) produces.\n\n> **Info: Prerequisites**\n>\n>\n> - The [Logos Blockchain app](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md) running on Basecamp version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later, with the node in **Online** mode.\n> - A funded node that has proposed at least one block. The lifecycle lane on the **Node** tab reaches **Earning** once a reward voucher is ready to claim.\n\n## What to expect\n\n- You can see how many leader reward vouchers are ready to claim in the **Rewards** tab and claim one with a single button press.\n- You can follow each claim in the **History** list until it reaches finality, and open its transaction in the **Explorer** tab.\n- You can claim mined proof-of-work tickets from the **Mining** tab, by hand or automatically.\n\n## Claim a reward voucher\n\nCheck what is ready to claim, submit a claim, then follow it until it settles.\n\n1. Click the **Rewards** tab.\n\n   - The **Ready to claim** card shows how many vouchers your wallet can claim now, with their approximate value before fees beneath.\n   - Click the **Ready to claim** card to list the vouchers, with the `cm` and `nf` values of each and the chain tip they were read at.\n\n   > **Info**\n   >\n   > If **Ready to claim** shows `0`, your node has not led a block yet, or every voucher has already been claimed. If it shows `—`, the node has not reported yet. Wait for more blocks; the card updates by itself.\n\n   ![The Rewards tab of a node that has no vouchers to claim yet](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/claim-leader-rewards-in-logos-blockchain-ui-app/rewards-ready.png)\n\n1. Click **Claim** next to **Vouchers**, or in the voucher list.\n\n   - One claim redeems one voucher, and the protocol picks which one.\n   - A **Claim submitted** notice shows the transaction hash. Click the copy button to copy it.\n   - The **Submitted** card counts the claim as **waiting to land** until its transaction is seen in a block.\n\n1. Under **History**, find the new claim.\n\n   - Each entry shows the amount, the **Slot**, the account it was **Paid to**, and links to its **Tx** and **Block**.\n   - A `Finalizes in …` badge shows how long until the claim is final. The badge disappears once it is.\n   - Select **Show only pending** to list only the claims that have not reached finality yet.\n\n1. Click the **Tx** link of the claim to open its transaction in the **Explorer** tab. Its operation is labelled **Leader Claim**.\n\n1. Once the claim is final, open the **Node** tab and check the **Earned** tile, which adds up the leader rewards this wallet has claimed. To see the balance of the account the reward was paid to, open **Wallet** > **Accounts** and click **Refresh**.\n\n## Claim mining rewards\n\nMining (the **Fund** button in the header) produces proof-of-work tickets instead of vouchers. A ticket pays nothing until it is claimed, and it expires if it is never claimed. Mining rewards are claimed from the **Mining** tab, not the **Rewards** tab.\n\n1. Click the **Mining** tab.\n\n   - **Ready to claim** shows how many mined tickets the node can still redeem, and how soon the nearest ones expire.\n   - **Awaiting payout** shows claims on their way to the chain.\n   - **Mining Rewards** shows what mining has paid so far, before fees.\n\n1. Check that **Auto-claim** is on. The node turns it on at startup whenever the config lists at least one auto-claim target, and a generated config lists your **PoWClaim** account, so it is on by default.\n\n   - Auto-claim pays into the auto-claim targets listed beneath the switch. You set them in the **Fund** step of the setup wizard, or under `pow` in the user config. Without a target, the switch stays disabled.\n   - The switch applies to the running node only. When you restart the node, it goes back to what the config says.\n\n1. To claim by hand, go to **Manual claim**. Leave the account set to **Let the node choose** (click **Clear** to get back to it), or pick one of your accounts, then click **Claim**.\n\n   - The result appears below the button as `Claim submitted: <hash>`, or `Claim failed: …`.\n   - Each claim then appears under **History**, as on the **Rewards** tab.\n\n   ![The Mining tab with Auto-claim on, Manual claim, and the claim history](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/claim-leader-rewards-in-logos-blockchain-ui-app/mining-manual-claim.png)\n\n## Troubleshooting leader rewards\n\n### Ready to claim shows 0\n\nThe node has not led a block yet, or every voucher has been claimed. A node only leads blocks once its stake has aged in, which can take up to two [epochs](https://docs.logos.co/get-started/glossary.md#epoch) after funding. Keep the node running; the card updates as new blocks arrive.\n\n### The Rewards tab shows `Start the node from the Node tab.`\n\nThe node is not running, so the app cannot read your vouchers. Click **Start Node** in the header and wait for the **Node** tab to read **Online**. While the node is still syncing, the notice reads `The node is still catching up.`\n\n### Clicking Claim shows Claim failed\n\nThe node rejected the claim, and the notice shows its error. Confirm that the node is **Online** and that **Ready to claim** is above `0` before retrying.\n\n### The claim stays in Submitted\n\nTransaction inclusion is not always immediate. Wait for more blocks. A claim that is never seen in a block is dropped from **Submitted** once the chain has moved well past it; submit a new claim if your voucher is still listed under **Ready to claim**.\n\n### The account balance has not changed after claiming\n\nThe balance reflects on-chain state, so it only changes once the claim transaction is in a block. Wait until the claim appears under **History**, then click **Refresh** in **Wallet** > **Accounts**. Expect slightly less than the voucher's value, because the fee for the claim comes off the reward.\n\n### The Mining tab shows `Nothing is claiming these tickets`\n\nMining is on but auto-claim is off, so mined tickets expire unclaimed. Switch **Auto-claim** on, or claim by hand under **Manual claim**.\n"},{"id":"blockchain/concepts/about-bedrock","title":"About Bedrock","summary":"Understand how Bedrock provides the consensus and shared context that Zones build on.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/concepts/about-bedrock","markdown_url":"https://docs.logos.co/blockchain/concepts/about-bedrock.md","permalink":"/blockchain/concepts/about-bedrock","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/concepts/about-bedrock.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Bedrock components"},{"depth":3,"text":"Cryptarchia"},{"depth":3,"text":"Mantle"}],"markdown":"# About Bedrock\n\n#### Understand how Bedrock provides the consensus and shared context that Zones build on.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nThe [Bedrock](https://docs.logos.co/get-started/glossary.md#bedrock) layer is the basis on top of which the broader [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) is built, operating as a large and decentralised peer-to-peer blockchain network. Bedrock functions as a solid foundation for [Zones](https://docs.logos.co/blockchain/concepts/about-zones.md) built on Logos, allowing Zone applications to leverage the Logos Blockchain's consensus guarantees. Bedrock also supports interoperability between Zones and Bedrock itself with support for asynchronous messaging and token bridging. By handling the logic allowing several parties to participate in sequencing a Zone, Bedrock contributes to the decentralisation of the Logos stack.\n\n## The basics\n\n- Bedrock is the foundational layer of the Logos Blockchain, operating as a decentralised peer-to-peer network.\n- Bedrock provides consensus on transaction ordering to Zones, as well as a common context for Zone interaction.\n- Participation in Bedrock is credibly neutral and private, ensuring open access for all participants.\n\n## Bedrock components\n\nBedrock is organised into three key components. The most basic of these is the peer-to-peer network that allows [Logos nodes](https://docs.logos.co/get-started/glossary.md#logos-node) to communicate with each other in a decentralised way.\n\n### Cryptarchia\n\nOn top of the peer-to-peer network sits [Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md), the Logos Blockchain’s consensus protocol used to reach an agreement about the state of the blockchain. As a [Private Proof of Stake](https://docs.logos.co/get-started/glossary.md#private-proof-of-stake) ([PPoS](https://docs.logos.co/get-started/glossary.md#ppos)) protocol, Cryptarchia gives all participants a proportional chance to propose a block, while making it infeasible to link blocks to their proposers both before and after the proposal.\n\n### Mantle\n\n[Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md), a Bedrock component that serves as the operating system of the Logos Blockchain, provides a minimal shared execution environment for Zones that allows them to interact with Bedrock. This includes operations like writing data to the blockchain, as well as a restricted ledger of [notes](https://docs.logos.co/get-started/glossary.md#note) to support [mining](https://docs.logos.co/get-started/glossary.md#mining), payments, and staking.\n\nZones make use of Mantle operations when posting their updates to the Logos Blockchain, as well as to communicate asynchronously with other Zones. Mantle ensures that Zones state updates are correctly ordered via the use of Logos [channels](https://docs.logos.co/get-started/glossary.md#channel). Logos channels also allow for several sequencers to sequence a Zone, and enable token bridging between Zones and Bedrock.\n"},{"id":"blockchain/concepts/about-cryptarchia","title":"About Cryptarchia","summary":"Understand how Cryptarchia reaches consensus while keeping block proposers private.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/concepts/about-cryptarchia","markdown_url":"https://docs.logos.co/blockchain/concepts/about-cryptarchia.md","permalink":"/blockchain/concepts/about-cryptarchia","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/concepts/about-cryptarchia.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Properties of Cryptarchia"},{"depth":3,"text":"Resilience via liveness"},{"depth":3,"text":"Proposer privacy"},{"depth":3,"text":"Low barrier to entry"},{"depth":2,"text":"How Cryptarchia works"},{"depth":3,"text":"Time units"},{"depth":3,"text":"Fork choice rule"},{"depth":3,"text":"Leadership election"}],"markdown":"# About Cryptarchia\n\n#### Understand how Cryptarchia reaches consensus while keeping block proposers private.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Cryptarchia](https://docs.logos.co/get-started/glossary.md#cryptarchia), the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain)'s consensus protocol, ensures that the entire Logos network reaches an agreement on the correct state of the blockchain. Cryptarchia uses a [Private Proof of Stake](https://docs.logos.co/get-started/glossary.md#private-proof-of-stake) ([PPoS](https://docs.logos.co/get-started/glossary.md#ppos)) consensus mechanism, making it infeasible to link nodes to blocks they propose. PPoS also makes it infeasible for adversaries to deduce a proposer's relative stake based on its activity. This separation reinforces the neutrality of the network, since nodes cannot be easily linked to any specific activity on the network. At the same time, Cryptarchia has no stake barrier for participating in consensus, fostering further decentralisation.\n\n## The basics\n\n- Cryptarchia is a Private Proof of Stake (PPoS) consensus protocol, making it infeasible to link nodes to blocks they propose and deduce their relative stake.\n- Cryptarchia maintains block production during failures, allowing the network to keep operating even if competing forks emerge temporarily.\n- Cryptarchia has low entry barriers, so validator nodes can run on a basic laptop with no minimum stake requirement.\n\n> **Note**\n>\n> While Cryptarchia is a fully Proof of Stake protocol, the Logos Blockchain also uses a Proof of Work mechanism to allow users to independently acquire notes. For more information, see [Mining notes](https://docs.logos.co/blockchain/concepts/about-mantle.md#mining-notes).\n\n## Properties of Cryptarchia\n\n### Resilience via liveness\n\nIn designing a resilient consensus protocol to use for the Logos Blockchain, it was necessary to decide whether to prioritise [safety or liveness](https://eprint.iacr.org/2016/889.pdf) in the event of a catastrophic failure. If safety is deemed a priority, the chain can never fork in order to ensure that the blockchain state is always agreed-upon, even if activity has to halt while the network recovers. Protocols that provide such safety guarantees generally rely on quorum-based consensus, which requires a permissioned set of participants with extensive communication and a low fault tolerance. These requirements introduce high barriers to entry that are antithetical to the Logos vision.\n\nPrioritising liveness means that block production will continue during a failure, but competing forks will be created before the network settles on one honest chain. These protocols involve participants systematically making local choices about which fork to follow, with no need for a permissioned network or for extensive communication. Accordingly, Cryptarchia was designed to prioritise liveness during a failure.\n\n### Proposer privacy\n\nCryptarchia provides PPoS consensus that makes it infeasible to discover the identity of a block proposer both before and after a proposal. A proposer’s identity is kept private before the proposal by using a secret leadership election. This mechanism selects consensus leaders without revealing the leader schedule ahead of time.\n\nFor full [proposer privacy](https://docs.logos.co/get-started/glossary.md#proposer-privacy), a secret leadership election is not enough: once a leader proposes a block, it is [trivial to link them](https://eprint.iacr.org/2021/409.pdf) to their proposed block without additional privacy measures in place. Cryptarchia is designed to work together with the [Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md), which makes it [computationally infeasible](https://lip.logos.co/blockchain/raw/blend-protocol.html#impact-of-the-blend-protocol-on-the-time-to-link-and-time-to-infer-the-stake) to link a proposer and their block. This property creates a much more powerful layer of privacy, resilience and neutrality.\n\n### Low barrier to entry\n\nIn order to achieve maximum decentralisation, Cryptarchia was designed to have low barriers to entry to encourage a greater circle of contributors. Participating as a Cryptarchia validator node is as simple as having the [Logos node](https://docs.logos.co/get-started/glossary.md#logos-node) application run in the background on a laptop, with a “set it and forget it” approach to maintenance. This design makes it easy to contribute to the security and continued operation of the Logos Blockchain.\n\n## How Cryptarchia works\n\n### Time units\n\nFollowing the [Ouroboros](https://www.drwx.org/papers/crypsinous.pdf) model, Cryptarchia divides time into basic units called slots that are grouped into larger units called epochs. Each [slot](https://docs.logos.co/get-started/glossary.md#slot) allows for the addition of at most one block to a given chain, while each new [epoch](https://docs.logos.co/get-started/glossary.md#epoch) refreshes the randomness and eligibility set used for the leadership election. Every Cryptarchia slot is 1 second long, and an epoch is about 7.5 days long. These time units are illustrated in the diagram below.\n\n![The time units used by the Logos Blockchain.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/about-cryptarchia/time-units.png)\n\n### Fork choice rule\n\nAs mentioned earlier, Cryptarchia’s preference for liveness produces competing forks fairly often - even under honest behaviour. The way Logos nodes decide on which fork represents the correct, or canonical, blockchain depends on how long that node has been offline. The node will compare parallel chains it sees on the network to its own preferred chain, switching to the observed chain if it is selected by the fork choice rule.\n\nWhen a Logos node is connected to the broader network and sees new honest blocks relatively quickly, it will use the online fork choice rule to select the honest chain. Under this rule, the node will select the chain with the most blocks - the “longest chain” - as long as it diverges from the node’s chain less than **k** blocks ago. This **k** parameter, known as the security parameter, describes how many blocks deep a transaction needs to be before it is considered immutable.\n\nWhen a Logos node is joining the network for the first time, or after a prolonged absence, it must use the bootstrapping fork choice rule. Like the online rule, the [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping) rule selects the longest chain when competing chains diverged from the node’s preferred chain less than **k** blocks ago. However, forks diverging more than **k** blocks ago are not dismissed. On the contrary, such a chain is selected if it has the most blocks (that is, is the most dense) in the period immediately after the fork. The contrast between these two rules can be seen in the diagram below.\n\n![The Cryptarchia bootstrapping fork choice rule.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/about-cryptarchia/fork-choice.png)\n\nThe bootstrapping rule ensures that honest parties can join or rejoin the protocol after prolonged absences without having to trust another node to select their canonical chain. During normal operations, such a costly fork choice rule is not needed, allowing active nodes to use the online rule without compromising on security.\n\n### Leadership election\n\nCryptarchia uses Logos notes to select block proposers. Each [note](https://docs.logos.co/get-started/glossary.md#note) that has existed since the beginning of the previous epoch is eligible to win the Cryptarchia slot lottery. The owner of a winning note can then propose a block. Notes held by anyone can be used for consensus, with no minimum value requirement.\n\nEach slot presents an opportunity for a block proposer to add a block to the chain, so long as they win the leadership election for that slot. The leadership election is run locally by each individual eligible note, without any public leadership schedule. Whether a particular note wins the leadership election for a given slot is determined by comparing a random “ticket” value to a threshold derived from the note’s relative stake. Due to the privacy properties of Cryptarchia, this relative stake relies on an estimate of the total participating stake derived from the block production rate.\n\nIf the ticket, generated by hashing the lottery randomness together with the note data, is less than the threshold value, that note is eligible to serve as a leader for that slot. The owner of a note that won an election can submit a block proposal, which will contain a zero-knowledge [Proof of Leadership](https://docs.logos.co/get-started/glossary.md#proof-of-leadership) ([PoL](https://docs.logos.co/get-started/glossary.md#pol)). Most slots will have no leader in order to allow parties to synchronise, while some will have one or even several leaders.\n"},{"id":"blockchain/concepts/about-mantle","title":"About Mantle","summary":"Understand how Mantle lets nodes and Zones interact with Bedrock.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/concepts/about-mantle","markdown_url":"https://docs.logos.co/blockchain/concepts/about-mantle.md","permalink":"/blockchain/concepts/about-mantle","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/concepts/about-mantle.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Operations and transactions"},{"depth":2,"text":"Mantle ledger"},{"depth":3,"text":"Mining notes"},{"depth":2,"text":"Logos channels"},{"depth":3,"text":"Immediate ordering"},{"depth":3,"text":"Decentralised sequencing"},{"depth":3,"text":"Token bridging"},{"depth":3,"text":"Cross-Zone messaging"}],"markdown":"# About Mantle\n\n#### Understand how Mantle lets nodes and Zones interact with Bedrock.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Mantle](https://docs.logos.co/get-started/glossary.md#mantle) is a [Bedrock](https://docs.logos.co/get-started/glossary.md#bedrock) component that serves as the operating system of Logos. It provides operations that allow nodes to participate in the [Blend Network](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md), as well as to enable [Zones](https://docs.logos.co/blockchain/concepts/about-zones.md) to interact with Bedrock. Mantle is also responsible for handling Logos notes, which are Bedrock-native fungible tokens.\n\n## The basics\n\n- Mantle serves as Bedrock's execution layer, providing operations that allow nodes to participate in Bedrock [Services](https://docs.logos.co/get-started/glossary.md#service), as well as an execution environment that allows for interactions between Zones and Bedrock.\n- Mantle maintains a limited ledger for fee payment, tracking fungible assets called notes with UTXO-based transactions.\n- Mantle supports channels, which enforce immediate transaction ordering for Zones and facilitate decentralised sequencing and token bridging.\n\n## Operations and transactions\n\nMantle operations are the way [Logos nodes](https://docs.logos.co/get-started/glossary.md#logos-node) interact with Bedrock Services, and allow Zones to interact with Bedrock. Operations are submitted to Mantle via transactions, with each transaction potentially including several operations. The cost of executing operations is paid for by gas fees, denoted in [notes](https://docs.logos.co/blockchain/concepts/about-mantle.md#mantle-ledger). All Logos nodes re-execute Mantle operations independently to ensure that the ledger state is updated correctly.\n\nLogos nodes use Mantle operations to indicate their participation in the Blend Network, with Mantle providing a locking mechanism used to incentivise correct behaviour by participants. The operations supported by Mantle include transferring notes, staking notes for participation in the Blend Network, paying rewards to compensate participants, and unstaking notes.\n\n[Inscription](https://docs.logos.co/get-started/glossary.md#inscription) operations can be used to write arbitrary data to the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain), enabling a variety of ways to leverage the blockchain as a permanent, decentralised record. Crucially, Mantle can be used as a censorship resistant [message](https://docs.logos.co/get-started/glossary.md#message) delivery mechanism for Zones, allowing Zone sequencers to send information to each other and engage in coordinated action. This form of message passing facilitates cross-Zone interaction including arranging atomic transactions and state updates.\n\n## Mantle ledger\n\nMantle maintains a restricted ledger that keeps track of fungible assets known as notes, which are bound to their owners. Notes are primarily used to pay for Mantle operations, but can also be bridged to equivalent Zone tokens. Mantle notes are stored by Logos nodes in a dictionary mapping notes to their unique note identifiers.\n\nNote transfers are effected by using transfer operations, which are based on the UTXO model. To transfer value, a sender spends their note and creates an equivalent new note belonging to the recipient. A spent note can never be spent again. These notes each have their own secret key for spending, and a corresponding public key for receiving new notes.\n\n### Mining notes\n\nOwning notes is a prerequisite to meaningful involvement in the Logos Blockchain, allowing users to participate in consensus, join the Blend Network, and post transactions. To allow users to independently acquire notes in a private, decentralised way, Mantle supports mining notes via a Proof of Work mechanism known as *[EmPoWering](https://docs.logos.co/get-started/glossary.md#empowering)*. Notes are mined by solving a cryptographic \"puzzle\" - similar to how Bitcoin is mined.\n\nTo mine a new note, a user chooses a value and hashes it together with the consensus randomness value for the current or previous [epoch](https://docs.logos.co/get-started/glossary.md#epoch). This hash is compared to a threshold value derived from a recent canonical block selected by the user - if the hash is below the threshold, the user is eligible to claim some notes from Mantle's reward pool. The difficulty of solving the puzzle (that is, how many tries it would take on average to find a winning value) is adjusted periodically to target an average of 10 claims per block. A user with a winning hash can submit their claim transaction via the Blend Network to make it infeasible to link the transaction with their identity.\n\n## Logos channels\n\nLogos channels, or channels for short, are lightweight virtual chains overlaid on top of the Logos Blockchain. The purpose of channels is to immediately enforce the correct ordering of transactions from Zones. These channels are implemented as permissioned, ordered logs of messages signed by a sequencer. These messages usually take the form of state updates from a particular Zone. Channels also provide several key features to Zones, which are described below.\n\n### Immediate ordering\n\nLogos channels ensure that their transactions will eventually be included on-chain in the correct order, regardless of how the Logos Blockchain may fork or reorganise. This allows new transactions that depend on earlier ones to be submitted immediately, without waiting for finality. An example with two channels is shown below:\n\n![A diagram illustrating how two channels are included in the Logos Blockchain.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/about-mantle/two-channels.png)\n\nIt is important to note that Logos channels can only be relied on if the sequencer is trusted to act honestly. In the absence of this assumption, users must wait for the true blockchain finality.\n\n### Decentralised sequencing\n\nA [channel](https://docs.logos.co/get-started/glossary.md#channel) may have one or several sequencers; in the latter case, authorised sequencers may take turns publishing messages according to a round-robin schedule, with unresponsive sequencers losing their turn via an automated timeout. Alternatively, sequencers can compete to update the Zone state via a \"first write wins\" model.\n\nA given threshold of sequencers may also modify the sequencer list or change other channel properties by signing a message together, allowing an honest majority to remove a malicious sequencer. This form of decentralised Zone sequencing distributes the potential for MEV extraction across a set of parties rather than concentrating it as in the single-sequencer rollup design.\n\n### Token bridging\n\nLogos channels also enable token bridging and inter-Zone token transfers for Zones via the use of channel notes. Channel notes are notes that can only be spent by a channel's accredited keys, created when note value is bridged to the channel's Zone as part of a deposit operation. Conversely, channel notes are destroyed during a withdrawal operation, with new Mantle notes of equivalent value credited to recipients. Logos channel note transfers can also be used to effect atomic token transfers between Zones. While channel notes cannot be locked for [SDP participation](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#service-declaration-protocol), they remain eligible for the consensus lottery.\n\n### Cross-Zone messaging\n\nSince channel messages can include arbitrary data and are publicly visible, they can also be used for asynchronous or synchronous messaging. Used as a messaging platform, Logos channels can be used to coordinate action between sequencers of a single channel, for example to agree to sign a message modifying the sequencer list. They can also be used to arrange action across several Zones, such as atomic token transfers or other coordinated state transitions.\n"},{"id":"blockchain/concepts/about-the-blend-network","title":"About the Blend Network","summary":"Understand how the Blend Network hides the link between a block proposer and their proposal.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/concepts/about-the-blend-network","markdown_url":"https://docs.logos.co/blockchain/concepts/about-the-blend-network.md","permalink":"/blockchain/concepts/about-the-blend-network","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/concepts/about-the-blend-network.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Objectives"},{"depth":2,"text":"How the Blend Protocol works"},{"depth":2,"text":"Cover traffic"},{"depth":2,"text":"Participating in the Blend Network"},{"depth":3,"text":"Node roles: core, edge, and broadcast"},{"depth":3,"text":"Sending Mantle transactions via Proof of Work"},{"depth":3,"text":"Service Declaration Protocol"},{"depth":3,"text":"Service Reward Distribution Protocol"}],"markdown":"# About the Blend Network\n\n#### Understand how the Blend Network hides the link between a block proposer and their proposal.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nThe [Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network) improves privacy protections for block proposers on top of [Cryptarchia](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md), the Logos [Private Proof of Stake](https://docs.logos.co/get-started/glossary.md#private-proof-of-stake) consensus protocol. Cryptarchia uses a private leadership election as a first line of defence against deanonymisation, but ensuring that proposer information cannot be easily inferred through network monitoring requires stronger privacy protections. The [Blend Protocol](https://docs.logos.co/get-started/glossary.md#blend-protocol) provides these: a privacy-preserving broadcasting protocol that makes it [computationally infeasible](https://lip.logos.co/blockchain/raw/blend-protocol.html#impact-of-the-blend-protocol-on-the-time-to-link-and-time-to-infer-the-stake) to link a message to its sender through network analysis. The Blend Network is the [Service](https://docs.logos.co/get-started/glossary.md#service) made up of nodes that opt in to run the Blend Protocol, and those nodes are rewarded for participating.\n\n## The basics\n\n- The Blend Network improves privacy protections for block proposers on top of Cryptarchia's Private Proof of Stake consensus.\n- Messages are wrapped in multiple layers of encryption and routed through random paths of nodes, each adding a random delay before forwarding.\n- Nodes must explicitly opt in through the Service Declaration Protocol and prove ownership of a [note](https://docs.logos.co/get-started/glossary.md#note) with a minimum stake to benefit from the strongest [proposer privacy](https://docs.logos.co/get-started/glossary.md#proposer-privacy).\n- Mantle transactions can also be sent through the Blend Network via a Proof of Work mechanism.\n\n## Objectives\n\nThe main objective of the Logos Blend Network is to reduce the probability of linking a block proposal with its proposer, which also translates to increasing the difficulty of learning the proposer’s relative stake. At the same time, the Blend Protocol was designed to minimise bandwidth usage on the network compared to general-use mixnets, and to maximise decentralisation by involving all nodes in the blending process. By doing so, the Blend Network increases the cost of attacking the network to deanonymise a block proposal without straining the network with high bandwidth usage.\n\nThe Blend Network supports the privacy of the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) as a whole, by improving privacy protections for block proposers, protecting against [adversaries](https://gwern.net/doc/cs/security/2009-syverson.pdf) with both a complete (global) and partial (local) view of the Logos Blockchain. The privacy protections provided by the Blend Network also substantially improves the privacy properties of Cryptarchia by making it even harder to learn a proposer’s relative stake - which would allow an attacker to estimate the likelihood of that node winning the leadership election.\n\n## How the Blend Protocol works\n\nThe Blend Network makes it difficult to link a block proposer to their proposal by having the message travel between several nodes before being revealed. [Blend nodes](https://docs.logos.co/get-started/glossary.md#blend-node) (also known as [core nodes](https://docs.logos.co/get-started/glossary.md#core-node)) must maintain a minimum number of connections with other nodes, and cannot exceed a maximum frequency of messages they can send - putting an upper bound on bandwidth usage.\n\nWhile dedicated participation in the Blend Network is reserved for declared Blend nodes, proposals and other messages can also be sent to it from regular Logos nodes. The following steps illustrate the process which a message goes through before being broadcast and included in the chain.\n\n1. The message sender selects a random path of nodes along which it will relay its message to the receiver, covering the message in layers of encryption for every node on the path.\n1.  The sender sends the layered message to every Blend node with which it maintains a peer-to-peer connection. This process, known as dissemination, is shown below.\n\n    ![The Blend Network using dissemination to relay proposal messages. Peer-to-peer relaying of messages is not shown for simplicity.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/about-the-blend-network/dissemination.png)\n\n1. When a Blend node receives a message, it checks that the message is unique and has not yet been seen, relaying it to its own peers. This ensures that every message is ultimately disseminated to the entire network.\n1. A Blend node that receives a message will also attempt to decrypt the message. If it is able to decrypt the outer layer of a message it receives, it relays this decrypted message (after a randomised delay) to its peers for the next node in the path to receive and decrypt.\n1. When the receiver finally receives its message, it decrypts this message and is able to retrieve the original payload. This payload is then broadcast to the Logos Blockchain as a block proposal.\n\nThis process of hiding messages under several layers of encryption and randomly delaying their propagation at each stage of dissemination is known as message blending. When a message is disseminated to all nodes, observers cannot determine which node was the intended receiver, even if they can identify the sender. Due to the layered encryption, observers also cannot determine which \"hop\" of the relay process the message is currently on. This decryption at each node in the path transforms messages, making it infeasible to link the incoming and outgoing messages based on their content.\n\nRandom delays are the other component of message blending. If messages are rare (as in a low bandwidth network like the Blend Network), then an observer may be able to link even encrypted messages if they are disseminated in close succession. To prevent this, every message is assigned a random delay by the node processing it, with each message being released in the order it is received once its delay period has passed. This makes it infeasible for an adversary to link incoming and outgoing messages based on their timing.\n\n## Cover traffic\n\nAn important way that the Blend Network obscures network patterns is by producing indistinguishable messages within a \"crowded\" network. To increase this effect in an environment where proposals are relatively rare, the Blend Network also produces artificial cover messages. Cover messages do not contain any meaningful payload and are generated by Blend nodes to increase network noise and to blend in with data messages that carry real payloads.\n\nCover messages mimic the behaviour of data messages, in that they are disseminated and processed by Blend nodes in the same manner. Blend nodes repeatedly encrypt random payload data to generate a cover message, which is then relayed to the next node in its selected path via dissemination. At each step in the transmission process, intended nodes decrypt, randomly delay, and disseminate cover messages without any indication that they may not be genuine. In fact, encrypted data and cover messages are completely indistinguishable even to adversary-controlled Blend nodes (a type of local observer). The hiding effect provided by [cover traffic](https://docs.logos.co/get-started/glossary.md#cover-traffic) is illustrated in the diagram below.\n\n![Disseminated Blend Network cover traffic (dotted lines) obscuring a data message (solid lines).](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/about-the-blend-network/cover-traffic-hiding-effect.png)\n\n_Disseminated Blend Network cover traffic (dotted lines) obscuring a data message (solid lines)._\n\nTo avoid the network getting congested with too much message traffic, the Blend Network places a quota limiting the number of cover messages produced by each node, as well as the number of nodes in the path of each cover and data message. Adherence to this quota is verified via a zero knowledge proof.\n\n## Participating in the Blend Network\n\nParticipation in the Blend Network as a Blend node is more complex than running a [Logos Blockchain node](https://docs.logos.co/get-started/glossary.md#logos-node). The protocol relies on an agreed-upon sets of active participants to ensure its correct operation, as Blend nodes must keep track of connections to other Blend nodes for message relaying. As a result, it is not possible to allow dynamic participation in the Blend Network as it exists for [Bedrock](https://docs.logos.co/get-started/glossary.md#bedrock). Participation in the Blend Network is therefore not incumbent upon a node unless it opts in.\n\n### Node roles: core, edge, and broadcast\n\nA Logos Blockchain node operates in one of three modes in terms of its engagement with the Blend Network. These roles are refreshed every [epoch](https://docs.logos.co/get-started/glossary.md#epoch) based on the set of active Blend declarations for that epoch:\n\n- **Core node**—a node that has opted in through the [Service Declaration Protocol](#service-declaration-protocol) and locked the required stake. Core nodes carry out the message blending: they relay and blend messages for others, resulting in maximum privacy for their own proposals. Core nodes receive rewards for their efforts via the [SRDP](#service-reward-distribution-protocol).\n- **Edge node**—a node sending a message through Blend (such as a block proposal or Mantle transaction) that has *not* declared as a core node but participates whenever there are enough core nodes in that epoch. An edge node sends its own messages through the core nodes for blending, gaining limited privacy without relaying for others.\n- **Broadcast**—a fallback method used when fewer than the minimum number of core nodes are active in an epoch. With too few core nodes to blend through, a sender will broadcast its message directly, without Blend privacy for that epoch.\n\nOnly the **core** role requires operator action—an SDP declaration, a locked stake, and a publicly reachable Blend port (see [Join the Blend Network as a core node](https://docs.logos.co/blockchain/blend/join-the-blend-network-as-a-core-node.md)). By contrast, a Logos Blockchain node participates as an edge node if the chain is online and enough core nodes exist, and falls back to broadcast mode otherwise. The minimum number of core nodes needed for edge participation is set by the network.\n\n### Sending Mantle transactions via Proof of Work\n\nIn addition to block proposals, nodes can also send Mantle transactions directly through the Blend Network. Using a Proof of Work mechanism known as *[EmPoWering](https://docs.logos.co/get-started/glossary.md#empowering)*, even a user without any notes benefits from some of the privacy provided by the Blend Network. However, as an edge node, their transaction will not have the same level of privacy protection as core nodes.\n\nTo send a Mantle transaction through the Blend Network, a user chooses a value and hashes it together with the consensus randomness value for the current or previous epoch. This hash is compared to a threshold value derived from the transactions in the epoch before last - if the hash is below the threshold, the user is eligible to send a Mantle transaction via the Blend Network. This transaction gets added to the mempool of the last node on its Blend path, eventually being included in a block in a way that makes it infeasible to link the transaction with the user that sent it.\n\nWhile Logos note transfers and most channel [inscriptions](https://docs.logos.co/get-started/glossary.md#inscription) are not private, *[EmPoWering](https://docs.logos.co/get-started/glossary.md#empowering)* can be used to inscribe arbitrary data on-chain in a private way. To do this, the user should first mine some notes and submit the claim transaction via PoW through the Blend Network. Then, they can submit an inscription transaction to a new Mantle [channel](https://docs.logos.co/get-started/glossary.md#channel) through Blend, which will add the desired data to the chain. To avoid linking this inscription with future ones, the user should then throw away the channel's signing key.\n\n### Service Declaration Protocol\n\nLogos nodes that choose to participate in the Blend Network explicitly declare their intent by using the Service Declaration Protocol (SDP). The goal of the SDP is to create a single repository of identifiers to determine which nodes have opted into the Blend Network at a given time.\n\nThe SDP provides a standardised mechanism for Logos nodes to declare their participation, demonstrate activity, and withdraw when desired. It operates around a schedule measured by consensus epochs. This protocol creates a single repository of identifiers used to establish secure communication between nodes and manage Service participation.\n\nThe SDP consists of three basic steps, each of which represents a type of message sent by a participating node to [Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md):\n\n- Declare: A node elects to participate in a given Service.\n- Active: To continue participating, the node must regularly send an \"active\" message. Nodes that have not sent an active message for a prolonged period of time have their declarations withdrawn.\n- Withdraw: A node withdraws its declaration and stops providing a Service.\n\nTo submit a Service declaration, a node must prove that it owns a note with a Service-dependent minimum stake value. This note is locked for the duration of the declaration, but remains eligible for consensus leadership. The stake requirement makes Service declarations sufficiently expensive to avoid spamming or Sybil attacks. Nodes participating in Services are assigned addresses (known as [locators](https://docs.logos.co/get-started/glossary.md#locator)) based on the [Multiaddr scheme](https://docs.libp2p.io/concepts/fundamentals/addressing/), allowing them to communicate securely while engaging in a Service.\n\n### Service Reward Distribution Protocol\n\nThe Service Reward Distribution Protocol (SRDP) enables deterministic, efficient, and verifiable reward distribution to nodes based on their participation in Bedrock Services. Like the SDP, it also operates around epochs. The SRDP process unfolds over three key phases, distributing rewards based on node activity from previous epochs. These phases are:\n\n- Activity tracking: Nodes participating in Services submit active messages to attest to their participation in the previous [epoch](https://docs.logos.co/get-started/glossary.md#epoch).\n- Reward calculation: At the end of the epoch, the system calculates rewards for nodes that participated in the previous epoch.\n- Reward distribution: Starting immediately after the epoch when activity messages are submitted, rewards are gradually distributed to active Service nodes. Each block includes one Mantle transaction that can distribute rewards to up to four nodes.\n\nThe selection of nodes receiving rewards in each block follows a deterministic, pseudo-random process based on the Cryptarchia epoch randomness from the start of the epoch. This approach ensures fairness and prevents manipulation of the distribution order.\n"},{"id":"blockchain/concepts/about-zones","title":"About Zones","summary":"Understand how Zones run custom, high-performance applications on the Logos Blockchain.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/concepts/about-zones","markdown_url":"https://docs.logos.co/blockchain/concepts/about-zones.md","permalink":"/blockchain/concepts/about-zones","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/concepts/about-zones.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"How Zones work"},{"depth":2,"text":"Additional features"},{"depth":3,"text":"Decentralised sequencing"},{"depth":3,"text":"Token bridging"},{"depth":3,"text":"Asynchronous messaging"},{"depth":2,"text":"Use cases"},{"depth":3,"text":"Logos Execution Zone"},{"depth":3,"text":"Other uses"}],"markdown":"# About Zones\n\n#### Understand how Zones run custom, high-performance applications on the Logos Blockchain.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nZones are customisable, high-performance blockchains for applications built on Logos. A [Zone](https://docs.logos.co/get-started/glossary.md#zone) defines its own state and execution environment, while relying on the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) for consensus and data availability. Zones can also use optional [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md) features such as token bridging and cross-Zone messaging. A sequencer updates a Zone's state by submitting state updates to the blockchain. Zones suit applications that need speed and customisation more than they need interoperability.\n\n## The basics\n\n- A Zone defines its own state and execution environment, while the Logos Blockchain provides consensus and data availability.\n- A sequencer, or a committee of sequencers, handles a Zone's transactions and submits state updates to the blockchain.\n- Zones can use tokens bridged from Bedrock and support cross-Zone transactions.\n\n## How Zones work\n\nZones let developers build performant applications with the freedom to define an execution environment that suits their needs, while the Logos Blockchain provides economic security and data availability that a Zone could not provide on its own. [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md) does not interpret Zone data. Instead, it provides operations through [Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md) that let Zone sequencers inscribe their state updates permanently to the blockchain.\n\nUnlike Ethereum Layer 2 chains, Zones do not rely on a smart contract on the Layer 1 to ensure the correctness of their state. A Zone can ensure correctness in several ways, including publishing zero knowledge validity proofs for its validators to verify, providing a challenge window for fraud proofs, or requiring Zone nodes to re-execute the state transition function.\n\n## Additional features\n\nThe Zone model described above is very similar to that of conventional [Sovereign Rollups](https://dba.xyz/rollups-are-l1s-l2s-a-k-a-how-rollups-actually-actually-actually-work/?ref=blog.nomos.tech). In fact, Zones can be built as pure sovereign rollups, with Bedrock only used for ordering and data availability guarantees. However, Bedrock also provides some optional functionality that somewhat increases the decentralisation and interoperability of Zones without significantly compromising the benefits of the sovereign rollup design.\n\n### Decentralised sequencing\n\nWhile a Zone can be implemented with just one sequencer updating the Zone state, this design allows the sequencer to take full advantage of any MEV and represents a centralised point of failure. On Logos, it is also possible to distribute sequencing rights to a permissioned set of parties. These sequencers can compete to be the first to publish the next state update, or take turns sequencing the Zone according to a round-robin schedule. This latter model provides more Bedrock-level protections for Zone users, including an automated timeout for unresponsive sequencers. Decentralised sequencing also allows a given threshold of sequencers to update the sequencer list to remove a malicious sequencer. All these operations are handled by Logos channels.\n\n### Token bridging\n\nUsers can also take advantage of built-in token bridging to transfer value between Bedrock and Zones. A Zone’s associated [channel](https://docs.logos.co/get-started/glossary.md#channel) maintains locked channel tokens, which together represent the total token value stored in the Zone. [Channel notes](https://docs.logos.co/get-started/glossary.md#channel-note) are updated as value is deposited to or withdrawn from a Zone, with notes of equivalent value destroyed or created on Bedrock as a result of these operations.\n\nNote that it is the Zone’s responsibility to ensure that token transfers are executed correctly within its own state. Bedrock only ensures that the total token value withdrawn from a Zone does not exceed the value deposited.\n\n### Asynchronous messaging\n\nZone sequencers can use channels to submit messages with arbitrary data, allowing them to communicate on-chain with other sequencers. Sequencers can use these messages to coordinate actions with other sequencers of the same Zone, for example to agree to sign a [message](https://docs.logos.co/get-started/glossary.md#message) modifying the sequencer list. They can also be used to arrange action across several Zones, such as atomic token transfers or other coordinated state transitions. In the latter case, sequencers must interpret all new messages and determine whether the message is directed to them. The Logos Blockchain provides a suggested message standard to make it easier for sequencers to interpret messages from other Zones, but anybody can define their own standard if they so desire.\n\n## Use cases\n\nZones can implement almost anything, ranging from applications to virtual machines that are home to many different applications. They are best suited for applications that require high performance and do not need strong interoperability.\n\n### Logos Execution Zone\n\nThe [Logos Execution Zone (LEZ)](https://docs.logos.co/lez.md) is the flagship Zone on the Logos Blockchain. It is the initial Zone for applications built using the complete Logos stack, including its messaging and storage modules. It also provides support for private accounts and [program](https://docs.logos.co/get-started/glossary.md#program) execution, with the state being verified via zero knowledge proofs. For more information about the Logos Execution Zone, see the page below:\n\n### Other uses\n\nIn addition to the LEZ, anybody can make a Zone on Logos. The Logos Team has already prepared a [Zone SDK](https://github.com/logos-blockchain/logos-blockchain/tree/master/zone-sdk) that makes it easy to build a Logos Zone. Some possibilities of Sovereign Zone applications include:\n\n- Self-Sovereign Digital Communities: Logos Blockchain can power the core identity, communication, and governance layers for online communities. This would involve:\n  - Self-Sovereign Identity (SSI) Solutions: Users can control their digital identities and share verifiable credentials privately and selectively.\n  - Secure Communication Channels: The Logos ecosystem can be used to build encrypted and censorship-resistant collaboration tools for community members.\n  - Decentralised Dispute Resolution: Building frameworks for resolving disputes and recording agreements between community members, potentially leveraging zero-knowledge proofs for privacy.\n- Gaming Applications: Games often rely on rapid state changes to function correctly, making them ideal for implementation as Zone applications on Logos.\n- Small-scale Networked Applications: Networked applications can use the Logos Blockchain to store their state updates on-chain, with users recreating the state locally based on these updates. This makes the Logos Blockchain a low cost, easy alternative for self-hosting - especially for small-scale applications.\n"},{"id":"blockchain/transfer-tokens/move-blockchain-assets-on-basecamp","title":"Move Blockchain assets on Basecamp","summary":"Get started with token transfers between wallet accounts directly from the dashboard UI.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/transfer-tokens/move-blockchain-assets-on-basecamp","markdown_url":"https://docs.logos.co/blockchain/transfer-tokens/move-blockchain-assets-on-basecamp.md","permalink":"/blockchain/transfer-tokens/move-blockchain-assets-on-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/transfer-tokens/move-blockchain-assets-on-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Start the node and open the transfer form"},{"depth":2,"text":"Read the transfer result"},{"depth":2,"text":"Troubleshooting dashboard transfers"},{"depth":3,"text":"The Send button stays disabled"},{"depth":3,"text":"The result shows `Error: Module not initialized.`"},{"depth":3,"text":"The result shows another `Error: …`"}],"markdown":"# Move Blockchain assets on Basecamp\n\n#### Get started with token transfers between wallet accounts directly from the dashboard UI.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure covers how to send funds from one of your wallet accounts to a recipient on the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) using the **Transfer** form of the Logos Blockchain [app](https://docs.logos.co/get-started/glossary.md#app) on Basecamp. It is intended for users who need to move tokens between accounts without using the CLI or crafting transactions manually.\n\n> **Info: Prerequisites**\n>\n>\n> - The [Logos Blockchain app](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md) running on Basecamp version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later, with the node in **Online** mode.\n> - At least one wallet account with a positive balance, for example one funded by [mining](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md#step-4-fund-your-node-and-propose-blocks).\n> - The recipient's 64-hex-character public key.\n\n## What to expect\n\n- You can pick any of your wallet accounts as the sender and see its balance in TestToken before you send.\n- You can submit a transfer and copy the resulting transaction hash from the result notice below the **Send** button.\n- You can confirm the transfer by finding its hash in the **Explorer** tab or on the testnet block explorer website, and by refreshing your account balances.\n\n## Start the node and open the transfer form\n\nEvery tab in the app is open whether or not the node is running, but the **Send** button stays disabled until the node is running. Start the node first, then open the form.\n\n1. If the node is not running, click **Start Node** in the header. Wait until the headline on the **Node** tab reads **Online**.\n\n1. Click the **Wallet** tab, then click **Transfer** in the left menu.\n\n   - While the node cannot answer, a notice at the top of the form says why, for example `Start the node from the Node tab.` or `The node is still catching up.`\n\n1. Select the sender account in the **From** dropdown.\n\n   - Each account shows its name, address, and balance.\n   - The balance of the selected account appears as **Available:** next to the **Amount (TestToken)** label.\n\n1. Enter the recipient's public key in the **To** field.\n\n1. Enter the amount to send, in TestToken, in the **Amount (TestToken)** field.\n\n   - If the amount is more than the account holds, the form shows `More than this account holds.` and **Send** stays disabled.\n\n   ![The Transfer form with From, To, and Amount filled in](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/move-assets-using-logos-blockchain-app/transfer-form.png)\n\n1. Click **Send**.\n\n   > **Warning**\n   >\n   > Transfers are irreversible; double-check the 64-hex recipient key and the amount before clicking **Send**.\n\n## Read the transfer result\n\n1. Check the notice below the **Send** button:\n\n   - On success, it reads **Transaction sent** and shows the transaction hash returned by the [module](https://docs.logos.co/get-started/glossary.md#module). Click the copy button to copy it to the clipboard.\n   - On failure, it reads **Transfer failed** and shows `Error: <message>` with the reason.\n\n   ![A Transaction sent notice with the transaction hash](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/move-assets-using-logos-blockchain-app/transfer-result.png)\n\n1. Confirm that the transaction is on chain, either in the app or on the web:\n\n   - In the app, open the **Explorer** tab, paste the hash in **Search a block id or transaction hash**, and press **Enter**. The result shows the **Transaction** and the block slot it was included in. If it reads `Nothing found for “<hash>”.`, the transaction is not in a block yet. Wait for a few blocks and search again.\n\n     ![The Explorer tab showing a transfer transaction](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/move-assets-using-logos-blockchain-app/explorer-search.png)\n\n   - In your browser, go to `https://testnet.blockchain.logos.co/web/explorer/transactions/<TX_HASH>`, replacing `<TX_HASH>` with the hash. The [testnet block explorer](https://testnet.blockchain.logos.co/web/explorer/) shows the block that includes the transaction, and its operation as **LedgerTransfer**. If it reads **Transaction not found**, wait for a few blocks and reload the page.\n\n1. Open **Wallet** > **Accounts** and click **Refresh** to confirm that the sender's balance has decreased.\n\n## Troubleshooting dashboard transfers\n\n### The Send button stays disabled\n\n**Send** needs a running node, a sender account, a recipient key, and an amount that the account can cover. Start the node from the header if the notice at the top of the form asks you to, then check each field.\n\n### The result shows `Error: Module not initialized.`\n\nThe app's connection to the blockchain module did not initialise at startup. Restart the app and try again.\n\n### The result shows another `Error: …`\n\nThe module rejected the transaction. Common causes are an insufficient balance to cover the amount and fee, or an invalid recipient key. Verify that the recipient key is exactly 64 hex characters, then retry.\n"},{"id":"blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp","title":"Bridge assets from Logos Blockchain to a Zone on Basecamp","summary":"Get started locking wallet notes into a Logos Zone, including the LEZ, using Basecamp.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp","markdown_url":"https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md","permalink":"/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Submit a deposit"},{"depth":2,"text":"Read the deposit result"},{"depth":2,"text":"Troubleshooting channel deposits"},{"depth":3,"text":"Confirm & deposit is disabled"},{"depth":3,"text":"The note list reads `This account holds no notes to deposit.`"},{"depth":3,"text":"The LEZ testnet checkbox is missing"},{"depth":3,"text":"Next stays disabled on Step 2 of 4"},{"depth":3,"text":"The wizard shows Deposit failed"},{"depth":3,"text":"The deposit does not show up in the LEZ wallet"}],"markdown":"# Bridge assets from Logos Blockchain to a Zone on Basecamp\n\n#### Get started locking wallet notes into a Logos Zone, including the LEZ, using Basecamp.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure covers how to lock one or more of your Logos Blockchain wallet [notes](https://docs.logos.co/get-started/glossary.md#note) (UTXOs) into a Logos [Zone](https://docs.logos.co/get-started/glossary.md#zone) (such as the [LEZ](https://docs.logos.co/get-started/glossary.md#lez)) using the Logos Blockchain app on Basecamp, and receive the resulting transaction hash. It is intended for wallet users on Testnet v0.3 who need to fund a [channel](https://docs.logos.co/get-started/glossary.md#channel) without hand-assembling note IDs, keys, and fees on a CLI.\n\n> **Info: Prerequisites**\n>\n>\n> - The [Logos Blockchain app](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md) running on Basecamp version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later, with the node in **Online** mode.\n> - At least one wallet account with a positive balance, for example one funded by [mining](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md#step-4-fund-your-node-and-propose-blocks).\n> - The channel ID of the target Zone. For the public LEZ testnet address, see [Testnet endpoints](https://docs.logos.co/get-started/testnet-endpoints.md).\n> - For an LEZ deposit, a public account in the [LEZ wallet app](https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md) to receive the funds.\n>\n\n## What to expect\n\n- You can select one or more wallet notes and lock their full value into a channel through a four-step wizard.\n- You can review the exact deposit payload, including channel ID, notes, keys, fee, and metadata, before submitting.\n- You can confirm the deposit succeeded by finding the returned transaction hash in the **Explorer** tab, where the transaction carries a **Channel Deposit** operation.\n- You can see a LEZ deposit arrive in your LEZ wallet about an hour after it is included in a block.\n\n## Submit a deposit\n\nThe wizard collects the deposit payload across two input steps and a review step, then submits it on confirmation. A **Step N of 4** counter in the top-right corner shows where you are.\n\n1. Click the **Wallet** tab, then click **Channel Deposit** in the left menu.\n\n   - The deposit wizard opens at **Step 1 of 4**.\n   - While the node cannot answer, a notice at the top of the wizard says why, for example `Start the node from the Node tab.`\n\n1. On **Step 1 of 4**, choose the account to deposit from in **Deposit from**.\n\n   - The account's notes load automatically. Choosing a different account reloads them.\n\n1. Select one or more notes. The counter below the list shows how many notes are selected and their total. **Next** is enabled once at least one note is selected; the full value of each selected note is consumed. Click **Next**.\n\n   ![Step 1 of 4 with one note selected](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/bridge-assets-from-logos-blockchain-to-zone-using-app/deposit-step1.png)\n\n1. On **Step 2 of 4**, enter the deposit parameters:\n\n   - **Channel ID hex**: the target channel. For the public LEZ testnet, paste the relevant address from [Testnet endpoints](https://docs.logos.co/get-started/testnet-endpoints.md). For any other Zone, paste the 64-hex-character channel ID.\n\n      > **Warning**\n      >\n      > The **LEZ testnet** checkbox in [app](https://docs.logos.co/get-started/glossary.md#app) versions 0.3.1 and 0.3.2 fills in `0303030303030303030303030303030303030303030303030303030303030303`, a channel the LEZ testnet no longer reads. A deposit sent to it locks your notes but never arrives on the LEZ, so leave the checkbox clear and paste the ID instead. To confirm the current ID, run the `getChannelId` call in [The LEZ testnet checkbox is missing](#the-lez-testnet-checkbox-is-missing).\n\n   - **Change goes to**: the account that receives any leftover value. It defaults to the account you deposit from.\n   - **Accounts funding the gas fee**: the accounts the node draws the transaction fee from. The account you deposit from is added for you. To add another, choose it in the **Account…** dropdown and click **Add**.\n   - **Max tx fee**: the maximum fee you allow, in TestToken.\n   - **Metadata (base58, optional)**: data for the Zone, usually specific to it. For the LEZ, paste the ID of the public account that receives the deposit: in the [LEZ wallet app](https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md#create-a-new-public-account), click the copy button next to one of your public accounts. For other Zones, leave it empty if the Zone does not need any.\n\n     > **Warning**\n     >\n     > A LEZ deposit without a public account ID in **Metadata** is locked on the Logos Blockchain but never credited on the LEZ.\n   - **Optional tip hex (leave empty for current tip)**: the chain tip the deposit is built against. Leave it empty to use the node's current tip, or click **Use query tip** to use the tip that the notes were loaded at.\n\n   - **Next** is enabled once the channel ID, change account, at least one funding account, and max fee are present, and any metadata entered is valid base58. Click **Next**.\n\n   ![Step 2 of 4 with the LEZ testnet channel ID and a LEZ account ID in Metadata](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/bridge-assets-from-logos-blockchain-to-zone-using-app/deposit-step2.png)\n\n1. On **Step 3 of 4**, review the exact payload: channel ID, notes to consume and total amount, change and funding public keys, max transaction fee, metadata, and tip. Then click **Confirm & deposit**.\n\n   ![Step 3 of 4, the deposit review](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/bridge-assets-from-logos-blockchain-to-zone-using-app/deposit-step3-review.png)\n\n   > **Warning**\n   >\n   > Review the information carefully. Deposits are irreversible once included in a block.\n\n## Read the deposit result\n\n1. On **Step 4 of 4**, wait while the wizard shows **Submitting deposit…**, then read the result:\n\n   - On success, the wizard shows **Deposit submitted** with the transaction hash. Click the copy button to copy it.\n   - On failure, the wizard shows **Deposit failed** with the error message.\n\n   ![Step 4 of 4 showing Deposit submitted](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/blockchain/assets/bridge-assets-from-logos-blockchain-to-zone-using-app/deposit-step4-result.png)\n\n1. Click **New deposit** to reset the wizard for another deposit.\n\n1. Confirm the transaction on chain. Open the **Explorer** tab, paste the transaction hash in **Search a block id or transaction hash**, and press **Enter**.\n\n   - The result shows the transaction and the slot of the block it was included in. Its operation is labelled **Channel Deposit** (`op 18`).\n   - If the result reads `Nothing found for “<hash>”.`, the transaction is not in a block yet. Wait for a few blocks and search again.\n\n1. For a LEZ deposit, wait for the funds to arrive in the LEZ wallet. The LEZ credits a deposit only once the block that includes it is [final](https://docs.logos.co/blockchain/concepts/about-cryptarchia.md#fork-choice-rule) on the Logos Blockchain, which takes 120 blocks, or about an hour on the testnet. The deposit is then credited in the next LEZ block.\n\n   - To check, open the [LEZ wallet app](https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md) and click **Refresh** in the **Accounts** panel. The public account whose ID you pasted in **Metadata** shows the deposited amount added to its balance.\n\n## Troubleshooting channel deposits\n\n### Confirm & deposit is disabled\n\nThe node is not running. Start it with **Start Node** in the header and wait for the **Node** tab to read **Online** before submitting.\n\n### The note list reads `This account holds no notes to deposit.`\n\nEither the account has no spendable notes, or the node was not running when you chose the account, so its notes were never loaded.\n\n- If the node was not running, start it and wait for it to reach **Online** mode. Then choose a different account in **Deposit from**, and choose yours again to reload its notes.\n- If the node was running, the account has nothing to deposit. Fund it first, or choose another account.\n\n### The LEZ testnet checkbox is missing\n\nThe app has no LEZ channel ID: either it could not read the one built into its `metadata.json`, or it was built from `master`, which has none. Get the channel ID from the LEZ testnet and paste it in **Channel ID hex**:\n\n```bash\ncurl https://testnet.lez.logos.co/ \\\n -H \"Content-Type: application/json\" \\\n -d \"{ \\\n     \\\"jsonrpc\\\": \\\"2.0\\\", \\\n     \\\"method\\\": \\\"getChannelId\\\", \\\n     \\\"params\\\": {}, \\\n     \\\"id\\\": 1 \\\n }\"\n```\n\n### Next stays disabled on Step 2 of 4\n\nA required field is missing or invalid:\n\n- The channel ID is not 64 hex characters. The field shows `A channel ID is 64 hex characters (32 bytes).`\n- No change account or funding account is selected, or **Max tx fee** is empty.\n- The metadata is not valid base58. The field shows `Invalid base58 input`.\n\n### The wizard shows Deposit failed\n\nThe module rejected the transaction. Common causes are insufficient funds to cover the selected notes plus the maximum transaction fee, an invalid channel ID or key, or a rejected or expired tip. The exact error is shown in the result step.\n\n### The deposit does not show up in the LEZ wallet\n\n- Less than an hour has passed since the deposit was included in a block. The LEZ credits it only once that block is final on the Logos Blockchain. Wait, then click **Refresh** in the **Accounts** panel of the LEZ wallet again.\n- **Metadata** did not hold the ID of a public LEZ account. The LEZ cannot credit such a deposit.\n"},{"id":"blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk","title":"Inscribe data on-chain using the Zone SDK","summary":"Learn how to write plain text as on-chain inscriptions using a simple Logos Zone.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk","markdown_url":"https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md","permalink":"/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Clone the Logos Blockchain repository"},{"depth":2,"text":"Step 2: Initialise the `ZoneSequencer` struct"},{"depth":2,"text":"Step 3: Handle channel events"},{"depth":2,"text":"Step 4: Publish data"},{"depth":2,"text":"Step 5: Build and run the sequencer"},{"depth":2,"text":"Next steps"}],"markdown":"# Inscribe data on-chain using the Zone SDK\n\n#### Learn how to write plain text as on-chain inscriptions using a simple Logos Zone.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nApplications built on Logos are implemented in execution environments known as [_Zones_](https://docs.logos.co/blockchain/concepts/about-zones.md), which post data _inscriptions_ on-chain via Logos channels. A Zone could host a versatile rollup with thousands of applications, such as the [Logos Execution Zone](https://docs.logos.co/lez.md), or it could be a simple, standalone Zone tracking the state of just one application.\n\nThe [**Zone SDK**](https://docs.logos.co/get-started/glossary.md#zone-sdk) is a ready-to-use toolbox that handles basic interactions with a Logos Zone. This tutorial shows how to create a simple Logos Zone that writes plain text as on-chain inscriptions.\n\n> **Info: Prerequisites**\n>\n>\n> - A running [Logos Blockchain node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) that has reached `Online` mode.\n> - A key in the node's wallet that holds funds, such as the `PoWClaim` key funded by [mining](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md#step-4-mine-tokens-to-fund-your-node). The node spends this key's [notes](https://docs.logos.co/get-started/glossary.md#note) to pay each inscription's transaction fee.\n> - [Git](https://git-scm.com/) and the [Rust toolchain](https://www.rust-lang.org/tools/install).\n\n## What to expect\n\n- You will initialise a `ZoneSequencer` that connects to your [Logos node](https://docs.logos.co/get-started/glossary.md#logos-node) and creates a new [channel](https://docs.logos.co/get-started/glossary.md#channel) for posting inscriptions.\n- You will implement event handling so the sequencer tracks which of its inscriptions have finalised and keeps a checkpoint to resume from.\n- You will publish plain-text messages as on-chain inscriptions and verify they are finalised in the channel.\n\n## Step 1: Clone the Logos Blockchain repository\n\nBefore you begin, clone the [logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) repository. The `tui-zone-tutorial` branch contains a skeleton implementation of the TUI Zone, built on the Zone SDK, with space to fill in the relevant code. A complete implementation of the TUI Zone exists on the `tui-zone-solution` branch.\n\n1.  Run the following in your desired path to clone the repository and switch to the tutorial branch:\n\n    ```bash\n    git clone https://github.com/logos-blockchain/logos-blockchain.git\n    cd logos-blockchain\n\n    # To follow along with the tutorial\n    git checkout tui-zone-tutorial\n    cd deployment/tui-zone\n    ```\n\n    - The `deployment/tui-zone` directory exists only on the `tui-zone-tutorial` branch, so check out the branch before you change into it.\n\n## Step 2: Initialise the `ZoneSequencer` struct\n\nSequencer functionality is provided via the `ZoneSequencer` struct from the Zone SDK, found in `logos-blockchain/zone-sdk/src/sequencer/zone_sequencer.rs`. When initialising this struct, provide the following arguments:\n\n- `channel_id: ChannelId`—the ID of the channel associated with the Zone.\n- `signing_key: Ed25519Key`—a key authorised to post updates to the channel.\n- `node: Node`—a Node struct referring to your Logos Blockchain node, together with the credentials to access it. Created by `NodeHttpClient::new()`.\n- `funding: FundingConfig`—the node wallet key that pays transaction fees, and places limits on how much the sequencer pays.\n- `checkpoint: Option<SequencerCheckpoint>`—\\[Optional] the checkpoint representing the most recently pushed channel update.\n\nBefore posting to a new channel, the sequencer must first generate an Ed25519 public/private key pair. **This initial public key defines the channel ID, while the private key becomes the first authorised signing key.** The channel is created when the sequencer posts a [message](https://docs.logos.co/get-started/glossary.md#message) with this channel ID, unless it already exists. Initially, only the sequencer with the signing key can post messages to the channel. Additional keys can be authorised via the [CHANNEL\\_CONFIG](https://nomos-tech.notion.site/v1-2-Mantle-Specification-2ce261aa09df805ea358d80c2046cf95) [Mantle](https://docs.logos.co/get-started/glossary.md#mantle) Operation.\n\nAfter the first channel message, further messages include a hash reference to the previous message in the channel. If a message is posted with an incorrect parent hash, it is rejected. **Providing a checkpoint lets the sequencer track the last message posted to its channel, allowing it to resume posting new updates after restarting.** A checkpoint is not necessary for the session during which a sequencer creates a new channel.\n\nEvery transaction the sequencer publishes must pay a fee. Before signing a transaction, the sequencer asks your node to fund it from a key in the node's wallet: the node adds a fee transfer and proves it, so the key's secret never leaves the node. `FundingConfig` sets how this works:\n\n- `funding_pk`—the ZK public key, held by the node's wallet, whose notes pay the fees. The skeleton reads it from the `--funding-pk` command-line option into `args.funding_pk`.\n- `change_pk`—where change from an atomic withdrawal goes. `None` sends it to `funding_pk`. Plain inscriptions do not use it.\n- `max_tx_fee`—the highest fee the sequencer pays for a single transaction.\n- `priority_fee_percent`—a reserve on top of the required fee, so the transaction still lands if fees rise slightly before it is included. `FundingConfig::DEFAULT_PRIORITY_FEE_PERCENT` is 12%.\n\n1.  Add a function to create a new private signing key in `src/lib.rs`:\n\n    ```rust\n    // Load signing key from file or generate a new one if it doesn't exist\n    //\n    // path: The path to the signing key file\n    fn load_or_create_signing_key(path: &Path) -> Ed25519Key {\n        if path.exists() {\n            let key_bytes = fs::read(path).expect(\"failed to read key file\");\n            assert!(\n                key_bytes.len() == ED25519_SECRET_KEY_SIZE,\n                \"invalid key file: expected {} bytes, got {}\",\n                ED25519_SECRET_KEY_SIZE,\n                key_bytes.len()\n            );\n            let key_array: [u8; ED25519_SECRET_KEY_SIZE] =\n                key_bytes.try_into().expect(\"length already checked\");\n            Ed25519Key::from_bytes(&key_array)\n        } else {\n            let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE];\n            rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);\n            fs::write(path, key_bytes).expect(\"failed to write key file\");\n            Ed25519Key::from_bytes(&key_bytes)\n        }\n    }\n    ```\n1.  Fill in the `run()` function in `src/lib.rs` with the following code that creates a channel with an associated sequencer:\n\n    ```rust\n    // Processing loop\n    //\n    // args: Setup info\n    pub async fn run(args: InscribeArgs) {\n\n        // Get node URL\n        let node_url: Url = args.node_url.parse().expect(\"invalid node URL\");\n\n        // Create new signing key or load existing one from path\n        let signing_key = load_or_create_signing_key(Path::new(&args.key_path));\n\n        // Derive channel ID\n        let channel_id = ChannelId::from(signing_key.public_key().to_bytes());\n\n        println!(\"TUI Zone Sequencer\");\n        println!(\"  Node:       {node_url}\");\n        println!(\"  Key:        {}\", args.key_path);\n        println!(\"  Channel ID: {}\", hex::encode(channel_id.as_ref()));\n        println!();\n\n        // Create initial Zone state & load checkpoint (to be implemented later in the tutorial)\n        let mut state = InMemoryZoneState::default();\n        let checkpoint = state.load_checkpoint().cloned();\n\n        // Connect to node\n        let node = NodeHttpClient::new(CommonHttpClient::new(None), node_url);\n\n        // Pay transaction fees from a funded key in the node's wallet\n        let funding = FundingConfig {\n            funding_pk: args.funding_pk,\n            change_pk: None,\n            max_tx_fee: GasCost::new(1_000_000),\n            priority_fee_percent: FundingConfig::DEFAULT_PRIORITY_FEE_PERCENT,\n        };\n\n        // Initialise ZoneSequencer\n        let mut sequencer = ZoneSequencer::init(channel_id, signing_key, node, funding, checkpoint);\n\n        // Wait to start reading from stdin until ready\n        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();\n        let mut stdin_rx = spawn_stdin_reader(ready_rx);\n        let mut ready_tx = Some(ready_tx);\n\n        ...\n\n    }\n    ```\n\n## Step 3: Handle channel events\n\nFor a centralised Zone—a single sequencer that owns its channel—event handling is minimal. There are no competing sequencers, and a single sequencer never loses a [slot](https://docs.logos.co/get-started/glossary.md#slot), so the sequencer only needs to track which of its inscriptions have finalised and keep a checkpoint so it can resume after a restart. It learns about both through the Zone SDK's `Event` stream.\n\nThe status of the sequencer's backfill process, transactions sent by the sequencer, and any updates to the Zone state are communicated via the Zone SDK's `Event`. These events are:\n\n- `Ready`—the sequencer is caught up and ready to accept updates. It fires once when the sequencer finishes catching up with the chain, and again each time it reconnects to the node.\n- `BlocksProcessed`—a new block was processed. Includes the latest `checkpoint` and the list of `finalized` transactions. It also carries the `deposits` observed in the block and a `channel_update` reporting inscriptions that entered or left the channel, but those are only relevant to bridging and to channels shared by several sequencers—**a centralised Zone ignores them**.\n- `TurnNotification`—the sequencer's turn to write in a turn-based decentralised sequencing scenario (not applicable to our example).\n\n1.  In `src/state.rs`, add the following struct to track Zone state in memory and maintain the checkpoint. It implements the skeleton's `ZoneState` trait, which the skeleton's `src/ui.rs` uses to render the state:\n\n    ```rust\n    // Keep track of Zone state in memory\n    //\n    // published: Inscriptions published by your sequencer, not yet finalised\n    // finalized: All finalised inscriptions\n    // checkpoint: Last message in Zone state—lets the sequencer resume after a restart\n    #[derive(Default)]\n    pub struct InMemoryZoneState {\n        published: Vec<Msg>,\n        finalized: Vec<Msg>,\n        checkpoint: Option<SequencerCheckpoint>,\n    }\n\n    impl InMemoryZoneState {\n        // Record a tx we just published locally, so the local view stays in\n        // sync with what the SDK accepted. Called at the publish-call site.\n        pub fn on_published(&mut self, info: &InscriptionInfo) {\n            self.published\n                .push(Msg::from_payload(info.this_msg, &info.payload));\n        }\n    }\n\n    impl ZoneState for InMemoryZoneState {\n        // Move our finalised publishes out of `published` and into `finalized`.\n        fn on_finalized(&mut self, inscriptions: &[InscriptionInfo]) {\n            for info in inscriptions {\n                if let Some(i) = self\n                    .published\n                    .iter()\n                    .position(|m| m.msg_id == info.this_msg)\n                {\n                    self.published.remove(i);\n                }\n                if !self.finalized.iter().any(|m| m.msg_id == info.this_msg) {\n                    self.finalized\n                        .push(Msg::from_payload(info.this_msg, &info.payload));\n                }\n            }\n        }\n\n        fn published(&self) -> &[Msg] {\n            &self.published\n        }\n\n        fn finalized(&self) -> &[Msg] {\n            &self.finalized\n        }\n\n        fn save_checkpoint(&mut self, checkpoint: SequencerCheckpoint) {\n            self.checkpoint = Some(checkpoint);\n        }\n\n        fn load_checkpoint(&self) -> Option<&SequencerCheckpoint> {\n            self.checkpoint.as_ref()\n        }\n    }\n    ```\n1.  Back in `src/lib.rs`, add a function to handle when the sequencer has finished [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping):\n\n    ```rust\n    // Defines initial post-bootstrapping behaviour\n    //\n    // state: Zone state\n    // ready_tx: The transmitter of the ready event to ready_rx\n    fn handle_ready(\n        state: &InMemoryZoneState,\n        ready_tx: &mut Option<tokio::sync::oneshot::Sender<()>>,\n    ) {\n        info!(\"Sequencer ready\");\n        if let Some(tx) = ready_tx.take() {\n            let _ = tx.send(());\n        }\n        println!(\"Ready.\");\n        println!();\n        println!(\"Type a message and press Enter to publish.\");\n        println!(\"Press Ctrl-D or type an empty line to exit.\");\n        println!();\n        ui::render_state(state);\n        ui::prompt();\n    }\n    ```\n1.  Add a function to apply finalised messages to the Zone state:\n\n    ```rust\n    // Apply finalised messages from chain to Zone state\n    //\n    // items: Finalised transactions\n    // state: Zone state\n    fn apply_finalized(items: &[FinalizedTx], state: &mut InMemoryZoneState) {\n\n        // TUI only cares about inscriptions for rendering; deposit, withdraw,\n        // config and channel transfer ops have no inscription payload.\n        let inscriptions: Vec<InscriptionInfo> = items\n            .iter()\n            .flat_map(|t| t.ops.iter())\n            .filter_map(|op| match op {\n                FinalizedOp::Inscription(i) => Some(i.clone()),\n                FinalizedOp::Deposit(_)\n                | FinalizedOp::Withdraw(_)\n                | FinalizedOp::Config(_)\n                | FinalizedOp::ChannelTransfer(_) => None,\n            })\n            .collect();\n        state.on_finalized(&inscriptions);\n        ui::render_state(state);\n        ui::prompt();\n    }\n    ```\n1.  Add a function to handle sequencer events and execute whichever of the above handlers is necessary for the current event:\n\n    ```rust\n    // Handle sequencer events\n    //\n    // event: current sequencer event\n    fn handle_event(\n        event: Event,\n        state: &mut InMemoryZoneState,\n        ready_tx: &mut Option<tokio::sync::oneshot::Sender<()>>,\n    ) {\n        match event {\n            Event::Ready => handle_ready(state, ready_tx),\n\n            // Centralized Zone: ignore `channel_update` — a single sequencer\n            // never loses a slot, so there is nothing to adopt or roll back.\n            // Just apply finalised inscriptions and persist the checkpoint.\n            Event::BlocksProcessed {\n                checkpoint, finalized, ..\n            } => {\n                if !finalized.is_empty() {\n                    apply_finalized(&finalized, state);\n                }\n                state.save_checkpoint(checkpoint);\n            }\n            Event::TurnNotification { .. } => {}\n        }\n    }\n    ```\n1.  Add the processing loop to `run()` to watch for events:\n\n    ```rust\n    pub async fn run(args: InscribeArgs) {\n\n        ...\n\n        loop {\n            tokio::select! {\n\n                // Watch for events\n                event = sequencer.next_event() => {\n                    handle_event(event, &mut state, &mut ready_tx);\n                 }\n\n                ...\n\n                // Ctrl+C exits the loop\n                _ = tokio::signal::ctrl_c() => {\n                    println!();\n                    break;\n                }\n            }\n        }\n\n        println!(\"Goodbye!\");\n    }\n    ```\n\n## Step 4: Publish data\n\nOnce the `ZoneSequencer` is set up, posting data to the channel is as easy as passing a vector of bytes to the sequencer handle's asynchronous `publish` function. `publish` first asks your node to fund the transaction, then queues it for posting and returns the queued [inscription](https://docs.logos.co/get-started/glossary.md#inscription) together with the current checkpoint. It returns `Error::Unavailable` while the sequencer is still catching up with the chain or reconnecting to the node, and an error if the node cannot fund the transaction.\n\n1.  Add the following code to process user input text in the `run` function in `src/lib.rs`:\n\n    ```rust\n    pub async fn run(args: InscribeArgs) {\n\n        ...\n\n        loop {\n            tokio::select! {\n\n                ...\n\n                // Get input text from stdin\n                input = stdin_rx.recv() => {\n\n                    // Handle unexpected input\n                    let Some(text) = input else {\n                        println!();\n                        break;\n                    };\n\n                    // Turn input into AppMessage wrapper. Includes unique ID for each transaction to avoid removing 'duplicate' messages\n                    // from mempool\n                    let msg = AppMessage::new(text);\n                    debug!(tx_uuid = %msg.tx_uuid, text = %msg.text, \"Publishing message\");\n\n                    // publish() takes bytes anyway, so must convert\n                    let Ok(inscription) = Inscription::try_from(msg.to_bytes()) else {\n                        error!(\"Message is too large to fit in an inscription\");\n                        continue;\n                    };\n\n                    // Publish data\n                    match sequencer.handle().publish(inscription).await {\n\n                        // Get result and checkpoint, update Zone state\n                        Ok((result, checkpoint)) => {\n                            let info = result.tx.inscription();\n                            debug!(msg_id = %hex::encode(info.this_msg.as_ref()), \"Published\");\n                            state.on_published(info);\n                            state.save_checkpoint(checkpoint);\n                            ui::render_state(&state);\n                            eprintln!(\"  \\x1b[90mpending...\\x1b[0m\");\n                            ui::prompt();\n                        }\n                        Err(lb_zone_sdk::sequencer::Error::Unavailable { reason }) => {\n                            warn!(\"publish rejected: {reason}\");\n                            eprintln!(\n                                \"  \\x1b[33msequencer is not ready (starting up or reconnecting), try again in a moment\\x1b[0m\"\n                            );\n                            ui::prompt();\n                        }\n                        Err(e) => {\n                            error!(\"failed to publish: {e}\");\n                            break;\n                        }\n                    }\n                }\n\n                ...\n\n            }\n        }\n\n        ...\n\n    }\n    ```\n\n## Step 5: Build and run the sequencer\n\n1.  Find the public key of the node wallet key that pays the fees. If your node mines, use its `PoWClaim` key, which is the `public_key` of the auto-claim target:\n\n    ```bash\n    logosctl call blockchain_module pow_status | jq -r .result.value | jq .auto_claim.targets\n    ```\n\n1.  Check that the key holds funds. Replace `<FUNDING_KEY>` with the key from the previous step:\n\n    ```bash\n    curl -s http://localhost:8080/wallet/<FUNDING_KEY>/balance | jq .\n    ```\n\n    - If the request returns `404`, the key holds no notes yet. [Mine tokens](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md#step-4-mine-tokens-to-fund-your-node) until it does.\n\n1.  With the code filled in, build and run the sequencer from the `deployment/tui-zone` directory. The binary is named `tui-sequencer`:\n\n    ```bash\n    cargo run --bin tui-sequencer -- --node-url <YOUR_NODE_URL> --funding-pk <FUNDING_KEY>\n    ```\n\n    - `--node-url` defaults to `http://localhost:8080`, so you can omit it for a local node.\n    - You can set `NODE_URL` and `FUNDING_PK` as environment variables instead of passing the options.\n\n    The sequencer connects to your node, derives the channel ID from your signing key, and prints `Ready.` once it has caught up with the chain. Type a message and press Enter to publish it as an on-chain inscription. The first message creates the channel.\n\n    Each message first appears under `Published` (pending). A block includes it within a few minutes, but it only moves to `Finalized` once that block is behind the last irreversible block (LIB). On the testnet the LIB trails the tip by about 44,000 slots, so this takes roughly 12 hours. Keep the sequencer running if you want to watch the move. Press Ctrl-D or enter an empty line to exit.\n\n    - To confirm that your messages are on-chain before they finalise, query the channel using the `Channel ID` the sequencer printed at startup: `curl -s http://localhost:8080/channel/<CHANNEL_ID> | jq .`. It returns `Channel not found` until a block includes your first message, then the channel's state, where `tip_slot` is the slot of your latest included inscription.\n\n    - If the sequencer exits with `failed to publish: network error: funding failed`, the node could not fund the transaction. Check that `--funding-pk` is a key in your node's wallet and that it holds funds.\n\n## Next steps\n\nFor more examples of what can be built with the Zone SDK, see the following links:\n\n- [Bridge assets between Logos Blockchain and a Zone](https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md)\n- [Operate a decentralised Zone with round-robin sequencer rotation](https://docs.logos.co/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation.md)\n"},{"id":"blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone","title":"Bridge assets between Logos Blockchain and a Zone","summary":"Move tokens between Logos Blockchain and a Zone using channels and the Zone SDK.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone","markdown_url":"https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md","permalink":"/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Create a channel"},{"depth":2,"text":"Step 2: Observe deposits from Bedrock"},{"depth":2,"text":"Step 3: Submit a withdrawal"},{"depth":3,"text":"Option 1: Submit a single-sequencer withdrawal"},{"depth":3,"text":"Option 2: Submit a multi-sequencer withdrawal"},{"depth":2,"text":"Step 4: Recover from a reorg"},{"depth":2,"text":"Next steps"}],"markdown":"# Bridge assets between Logos Blockchain and a Zone\n\n#### Move tokens between Logos Blockchain and a Zone using channels and the Zone SDK.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\n[Logos Zones](https://docs.logos.co/blockchain/concepts/about-zones.md) are customizable, high-performance blockchains for applications built on Logos. [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) [notes](https://docs.logos.co/get-started/glossary.md#note) can be bridged from [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md) to Zones. A Zone's [channel](https://docs.logos.co/get-started/glossary.md#channel) holds the bridged funds as [**channel notes**](https://docs.logos.co/get-started/glossary.md#channel-note): ledger notes registered to the channel, which only the channel's accredited keys can spend.\n\nThis procedure covers creating a channel, depositing notes from the Blockchain into a Zone, and withdrawing notes from a Zone back to the Blockchain, using the [Zone SDK](https://docs.logos.co/get-started/glossary.md#zone-sdk)'s `ZoneSequencer`. It applies to Zone developers building sequencers and indexers; the Zone itself defines how the channel's funds map to its internal accounts, while the SDK only surfaces the on-chain events.\n\nBridging has two directions:\n\n- **Deposit** (Blockchain to Zone). A user deposits notes into a channel with a `ChannelDeposit` operation. Each deposited note is re-created as a channel note, and the Zone sequencer credits the user inside the Zone.\n- **Withdraw** (Zone to Blockchain). The Zone sequencer submits a `ChannelTransfer` that moves channel notes to the recipients, together with a `ChannelWithdraw` that releases the recipients' notes. The released notes become regular notes on the Blockchain, owned by the recipients' keys.\n\n> **Info: Prerequisites**\n>\n>\n> - A running [Logos Blockchain node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) reachable over HTTP, with a funded key in its wallet to pay transaction fees.\n> - The Zone SDK (`lb_zone_sdk`) and `lb_core` crates from release 0.3.2 added to your project.\n> - You have completed the [Zone SDK inscription tutorial](https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md).\n\n## What to expect\n\n- You can create a channel and have it recognised on-chain without a separate deployment transaction.\n- You can observe deposits on your channel, and credit them either once they are finalised or straight away by pinning them.\n- You can submit single- or multi-signature withdrawals and confirm them once finalised, including after a reorg.\n\n## Step 1: Create a channel\n\nA channel is created automatically the first time an operation references a previously unseen `ChannelId`. Whoever signs that first operation becomes the sole accredited key, and the channel starts with `configuration_threshold = 1`, `transfer_threshold = 1`, and no channel notes.\n\n1. Generate a [`Ed25519Key`](https://github.com/logos-blockchain/logos-blockchain/blob/0.3.2/kms/keys/src/keys/ed25519/mod.rs) for your sequencer. The private key is used to sign inscriptions and channel operations, while the public key defines the `ChannelId`. An example implementation is shown below:\n\n   ```rust\n   // Generate Ed25519 key pair\n   let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE];\n   rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);\n   std::fs::write(&your_key_file_path, key_bytes).expect(\"failed to write key file\");\n   let signing_key = Ed25519Key::from_bytes(&key_bytes);\n\n   // Derive channel ID\n   let channel_id = ChannelId::from(signing_key.public_key().to_bytes());\n   ```\n\n   The bridging-relevant fields on `ChannelState` in the [Mantle](https://docs.logos.co/get-started/glossary.md#mantle) specification:\n\n   | Field                | Purpose                                                              |\n   | -------------------- | -------------------------------------------------------------------- |\n   | `transfer_threshold` | Minimum number of accredited-key signatures needed to authorise a channel transfer or withdrawal. |\n   | `accredited_keys`    | The committee that may sign transfers and withdrawals. |\n\n1. Initialise a `ZoneSequencer` with a `FundingConfig` and publish the first [inscription](https://docs.logos.co/get-started/glossary.md#inscription) inside your event loop once `Event::Ready` has fired. The channel is created on-chain automatically, naming this sequencer's key as the sole accredited key.\n\n    ```rust\n    use lb_zone_sdk::{\n        CommonHttpClient,\n        adapter::NodeHttpClient,\n        sequencer::{FundingConfig, ZoneSequencer},\n    };\n\n    // Connect to the Logos Blockchain node.\n    let node = NodeHttpClient::new(\n        CommonHttpClient::new(None),\n        \"http://localhost:8080\".parse()?,\n    );\n\n    // Every publish-type call funds its transaction from the node's wallet.\n    let funding = FundingConfig {\n        funding_pk,\n        // Where an atomic withdrawal's change goes; `None` sends it to `funding_pk`.\n        change_pk: None,\n        max_tx_fee: 1_000_000.into(),\n        priority_fee_percent: FundingConfig::DEFAULT_PRIORITY_FEE_PERCENT,\n    };\n\n    // Initialise the sequencer for this channel.\n    let mut sequencer = ZoneSequencer::init(channel_id, signing_key, node, funding, None);\n\n    // Inside the event loop, once `Event::Ready` has fired:\n    // publishing the first inscription creates the channel just-in-time.\n    let (result, checkpoint) = sequencer.handle().publish(genesis_zone_block).await?;\n    ```\n\n    - `funding_pk` is the ZK public key of a funded key in the connected node's wallet. The node builds and proves the fee transfer, so the key's secret never leaves the node.\n    - `priority_fee_percent` is a reserve over the required fee that absorbs normal fee movement. Only the part left over after the final fee is charged becomes a priority tip. The default is 12%, and `max_tx_fee` caps the total.\n    - To change other sequencer settings, such as `resubmit_interval`, build a `SequencerConfig` with `SequencerConfig { ..SequencerConfig::new(funding) }` and pass it to `ZoneSequencer::init_with_config`.\n\n    `publish` funds the transaction, then queues it in the sequencer's pending set; the post reaches the node the next time the event loop polls `next_event`. The returned `PublishResult` and checkpoint carry everything you need to record this publish in your outbox.\n\n1. (Optional) Reconfigure the channel by calling `sequencer.handle().channel_config(..).await` with new keys, timing, and thresholds. See [Update the committee with a channel config op](https://docs.logos.co/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation.md#step-3-update-the-committee-with-a-channel-config-op).\n\n## Step 2: Observe deposits from Bedrock\n\nA deposit happens when a Bedrock user submits a transaction with a `ChannelDeposit` operation naming the target `channel`, the consumed `inputs`, and opaque `metadata` that the Zone interprets, such as a recipient address. On-chain execution spends the inputs and re-creates each one as a channel note with the same value and key.\n\n1. Watch for finalised deposits inside your event loop. The Zone SDK surfaces every finalised deposit on your channel as a `FinalizedOp::Deposit(DepositInfo)` inside the `finalized` field of `Event::BlocksProcessed`.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{Event, FinalizedOp};\n\n   if let Event::BlocksProcessed { finalized, .. } = event {\n       // Iterate over every finalised transaction in this batch of blocks.\n       for tx in finalized {\n           for op in tx.ops {\n               if let FinalizedOp::Deposit(deposit) = op {\n                   // Credit the user inside the Zone according to the Zone's internal rules.\n                   println!(\n                       \"Deposit of {} with metadata {:?}\",\n                       deposit.amount, deposit.metadata,\n                   );\n               }\n           }\n       }\n   }\n   ```\n\n   `Event::BlocksProcessed` fires once per ingested block, whether live or backfilled, and `finalized` only carries items at or below the last irreversible block (LIB), so deposits surfaced here cannot be re-orged off the chain.\n\n1. Credit the user inside the Zone according to the Zone's internal state transition function rules.\n\n1. (Optional) Credit a deposit before it is finalised by **pinning** it. `Event::BlocksProcessed` also carries `deposits`: the deposits observed in the block just processed, each with its `op_id`, `amount`, `metadata`, and the channel notes it created. Pin a deposit with `publish_pin_deposit(inscription, consumed_notes)`, which bundles an inscription with a channel transfer that consumes the deposit's notes, so the bundle can only land on a branch where the deposit exists.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{ChannelUpdateTx, Error, Event};\n\n   if let Event::BlocksProcessed { channel_update, deposits, .. } = event {\n       observed.extend(deposits.iter().cloned());\n\n       // A shed pin surfaces as orphaned: forget it so it is pinned again.\n       for tx in channel_update.orphaned() {\n           if let ChannelUpdateTx::PinDeposit(info) = tx {\n               pinned.remove(&deposit_of(info));\n           }\n       }\n\n       for deposit in &observed {\n           if pinned.contains(&deposit.op_id) {\n               continue;\n           }\n           let notes = deposit.notes.iter().map(|note| note.note_id).collect();\n           match sequencer.handle().publish_pin_deposit(pin_payload_for(deposit), notes).await {\n               Ok(_) => { pinned.insert(deposit.op_id); }\n               Err(Error::Network(_)) => {} // Not on this branch right now.\n               Err(e) => return Err(e),\n           }\n       }\n   }\n   ```\n\n   - Deposits in `deposits` are observations, not finalised state: a branch change can surface the same deposit again or reorg it out.\n   - If the deposit is not on the current branch, `publish_pin_deposit` returns `Error::Network` and posts nothing. Pin it again on a later event.\n   - The pin surfaces as `ChannelUpdateTx::PinDeposit` in `channel_update.adopted()`, and in `channel_update.orphaned()` if it is shed, in which case pin it again.\n   - `DepositLifecyclePolicy` in `tests/src/cucumber/steps/zone/operations/deposit_policy.rs` in the `logos-blockchain` repository is the reference implementation.\n\n## Step 3: Submit a withdrawal\n\nA withdrawal is initiated inside the Zone and lands on-chain as two operations in one transaction:\n\n- a `ChannelTransfer` that spends channel notes covering the amount and creates the recipients' notes, plus any change, as new channel notes;\n- a `ChannelWithdraw` that names the recipients' notes by ID and releases them, so they stay on-chain as regular notes owned by the recipients' keys.\n\nEach of the two carries a `ChannelMultiSigProof` with `ChannelState.transfer_threshold` signatures, at most one per accredited key. The process is different for single-sequencer Zones and for Zones whose transfer threshold is greater than one.\n\n### Option 1: Submit a single-sequencer withdrawal\n\nThis option applies only when `ChannelState.transfer_threshold == 1`.\n\n1. Describe the withdrawal by building a `WithdrawArg` with the recipient `Outputs`, and choose which channel notes pay for it with `WithdrawInputs`:\n\n   - `WithdrawInputs::Auto` lets the SDK pick covering notes from the channel notes it tracks.\n   - `WithdrawInputs::Explicit(note_ids)` names the exact notes, for example chosen from `sequencer.channel_wallet()`, which lists each tracked note's ID, value, key, and creation slot.\n\n   ```rust\n   use lb_core::mantle::{Note, ledger::Outputs};\n   use lb_zone_sdk::sequencer::{WithdrawArg, WithdrawInputs};\n\n   // Describe what to withdraw: a single note to the recipient.\n   let withdraw = WithdrawArg {\n       outputs: Outputs::new([Note::new(50, recipient_pk)]),\n   };\n   ```\n\n   The SDK fills in the `channel_id` and signs with this sequencer's accredited key.\n\n1. Submit the inscription bundled with the withdrawal by calling `sequencer.handle().publish_atomic_withdraw(..)`.\n\n   ```rust\n   // Inside the event loop: submit the inscription bundled with the withdrawal.\n   let (result, checkpoint) = sequencer\n       .handle()\n       .publish_atomic_withdraw(\n           inscription_payload, // the Zone block this withdrawal goes with\n           vec![withdraw],\n           WithdrawInputs::Auto,\n       )\n       .await?;\n   ```\n\n   Because the inscription, the transfer, and the withdrawal share one transaction, they become adopted, orphaned, or finalised as a unit, so the Zone block recording the withdrawal and the on-chain debit cannot drift apart.\n\n   > **Info**\n   >\n   > `publish_atomic_withdraw` returns the `PublishResult` as soon as the bundle is queued. For a single-signature bundle, `PublishResult.tx` is a `PendingTx::AtomicWithdraw(AtomicWithdrawInfo)` carrying the inscription, the bundled withdraw operations, and the recipient `outputs`. Record it in your outbox straight away.\n\n1. Check the finalised transactions in `Event::BlocksProcessed.finalized` against the pending transaction's `tx_hash`. Because it is a bundle, the inscription, the channel transfer, and the withdrawal all appear in the same `tx.ops` once the withdrawal is finalised.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{Event, FinalizedOp};\n\n   if let Event::BlocksProcessed { finalized, .. } = event {\n       for tx in finalized {\n           // Check if tx.tx_hash matches the pending tx_hash in your outbox.\n           for op in tx.ops {\n               match op {\n                   FinalizedOp::Inscription(info) => {\n                       // The Zone block carried with the withdrawal.\n                       println!(\"Inscribed {:?} in tx {:?}\", info.this_msg, info.tx_hash);\n                   }\n                   FinalizedOp::ChannelTransfer(transfer) => {\n                       // The channel notes moved to the recipients.\n                       println!(\"Transferred {:?} in tx {:?}\", transfer.op, transfer.tx_hash);\n                   }\n                   FinalizedOp::Withdraw(withdrawal) => {\n                       // The recipients' notes released from the channel.\n                       println!(\"Withdrawn {:?} in tx {:?}\", withdrawal.op, withdrawal.tx_hash);\n                   }\n                   _ => {}\n               }\n           }\n       }\n   }\n   ```\n\n### Option 2: Submit a multi-sequencer withdrawal\n\nWhen `transfer_threshold > 1`, no single sequencer can authorise a withdrawal alone. The proposing sequencer prepares a funded bundle, collects signatures from the other accredited key holders, and submits the bundle once it has enough.\n\n> **Info**\n>\n> Defining the committee transport, meaning how prepared bundles and signatures are exchanged between sequencers, is outside the Zone SDK's scope.\n\n1. Prepare the bundle by calling `handle.prepare_atomic_withdraw(..)` with the same arguments as `publish_atomic_withdraw`. The SDK builds and funds the transaction and returns a `PreparedAtomicBundle`. The bundled inscription is turn-gated, so prepare and submit from the sequencer whose turn it is to write.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{WithdrawArg, WithdrawInputs};\n\n   // 1. Build and fund the bundle for external signing.\n   let prepared = sequencer\n       .handle()\n       .prepare_atomic_withdraw(\n           inscription_payload,\n           vec![WithdrawArg { outputs }],\n           WithdrawInputs::Auto,\n       )\n       .await?;\n   ```\n\n1. Share the prepared bundle with the other accredited key holders. Each signer calls `prepared.sign_with(&key)`, which returns an `IndexedSignature` indexed against the channel's current accredited keys. A key holder without a running sequencer can call `sign_prepared(&key, &prepared.accredited_keys, &prepared.sign_payload)` instead.\n\n   ```rust\n   use lb_zone_sdk::sequencer::IndexedSignature;\n\n   // 2. Distribute `prepared` and collect the committee's signatures.\n   //    Transport is application-defined.\n   let collected: Vec<IndexedSignature> = collect_signatures_from_committee(&prepared).await?;\n   ```\n\n1. Gather `prepared.signing_threshold` signatures, which is the channel's current `transfer_threshold`, into `IndexedSignatures` and submit them with `handle.submit_atomic_bundle(prepared, signatures)`.\n\n   ```rust\n   use lb_zone_sdk::sequencer::IndexedSignatures;\n\n   // 3. Order the signatures by key index, at most one per key, and submit.\n   let signatures = IndexedSignatures::try_from_iter(collected.into_iter().map(Into::into))?;\n   let (result, checkpoint) = sequencer\n       .handle()\n       .submit_atomic_bundle(prepared, signatures)?;\n   ```\n\n   If the channel's configuration or message tip changed since the bundle was prepared, `submit_atomic_bundle` returns `Error::ChannelStateChanged`. Prepare the bundle again and collect new signatures.\n\n1. Match the finalised transaction by `tx_hash` once it appears in `Event::BlocksProcessed.finalized`, the same way as in the single-signature flow in Option 1.\n\n## Step 4: Recover from a reorg\n\nA reorg can orphan the parent inscription of a withdrawal, which invalidates the original signed transaction.\n\n1. Watch for orphaned transactions in the `channel_update` field of `Event::BlocksProcessed`. When entries leave the channel, the update is a `ChannelUpdate::Conflict`, and `channel_update.orphaned()` lists the abandoned transactions.\n\n1. Republish a single-signature withdrawal by calling `publish_atomic_withdraw` again with the same inscription payload and a `WithdrawArg` rebuilt from the bundle's `outputs`. The SDK refills the inscription parent and picks covering channel notes again from the current on-chain state.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{ChannelUpdateTx, Event, WithdrawArg, WithdrawInputs};\n\n   if let Event::BlocksProcessed { channel_update, .. } = event {\n       for tx in channel_update.orphaned() {\n           if let ChannelUpdateTx::AtomicWithdraw(info) = tx {\n               // Republish with the same inscription payload and recipients;\n               // the SDK refreshes the parent and the channel notes it spends.\n               let (result, checkpoint) = sequencer\n                   .handle()\n                   .publish_atomic_withdraw(\n                       info.inscription.payload.clone(),\n                       vec![WithdrawArg { outputs: info.outputs.clone() }],\n                       WithdrawInputs::Auto,\n                   )\n                   .await?;\n\n               // Record `result` + `checkpoint` exactly as on the original publish.\n           }\n       }\n   }\n   ```\n\n1. Record the new `result` and `checkpoint` in your outbox exactly as you did for the original publish, and match the finalised transaction against the new `tx_hash`.\n\n> **Note**\n>\n> For multi-signature bundles the orphan event carries the same `ChannelUpdateTx::AtomicWithdraw` data, whether the bundle was mined and reorged out, or never mined and shed once its parent [slot](https://docs.logos.co/get-started/glossary.md#slot) was taken by a competing entry. The SDK cannot re-sign the transaction, so recovery means running the `prepare_atomic_withdraw` → collect signatures → `submit_atomic_bundle` flow again against the new parent, with channel notes that are still unspent.\n\n## Next steps\n\nFor more examples of what can be built with the Zone SDK, see the following link:\n\n- [Operate a decentralised Zone with round-robin sequencer rotation](https://docs.logos.co/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation.md)\n"},{"id":"blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation","title":"Operate a decentralised Zone with round-robin sequencer rotation","summary":"Get started with multi-sequencer channels using the Zone SDK and round-robin slot windows.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation","markdown_url":"https://docs.logos.co/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation.md","permalink":"/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/zone-sdk/operate-decentralised-zone-with-round-robin-sequencer-rotation.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Size the rotation parameters"},{"depth":2,"text":"Step 2: Publish inscriptions and monitor turn state"},{"depth":2,"text":"Step 3: Update the committee with a channel config op"},{"depth":2,"text":"Next steps"}],"markdown":"# Operate a decentralised Zone with round-robin sequencer rotation\n\n#### Get started with multi-sequencer channels using the Zone SDK and round-robin slot windows.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis tutorial covers how to configure and operate a [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) [channel](https://docs.logos.co/get-started/glossary.md#channel) with a committee of accredited sequencers that take turns publishing inscriptions to a Logos [Zone](https://docs.logos.co/get-started/glossary.md#zone). It is intended for zone developers using the [Zone SDK](https://github.com/logos-blockchain/logos-blockchain/tree/0.3.2/zone-sdk) who need to move from a single-sequencer setup to a decentralised one.\n\nThe Zone SDK currently supports **round-robin** rotation only. Each sequencer publishes inscriptions for `posting_timeframe` slots before the rotation advances to the next sequencer in the `accredited_keys` list. Other scheduling schemes (such as First-Write-Wins) are not yet available.\n\n> **Info: Prerequisites**\n>\n>\n> - An existing application using the Zone SDK, such as an [inscription program](https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md).\n> - At least one `Ed25519PublicKey` generated per committee member.\n\n## What to expect\n\n- You can publish inscriptions from any sequencer at any time; the SDK holds pending transactions and drains them automatically when the sequencer's turn arrives.\n- You can monitor each sequencer's turn status in real time using `subscribe_turn_to_write` or `subscribe_channel_view`.\n- You can update the accredited key list to add or remove sequencers by submitting a `ChannelConfigOp`.\n\n## Step 1: Size the rotation parameters\n\nChoose values for `posting_timeframe` and `posting_timeout` before creating or reconfiguring the channel. These fields on [`ChannelState`](https://lip.logos.co/blockchain/raw/bedrock-v1.1-mantle-specification.html#message-ordering) control the rotation cadence and inactive-sequencer skip behaviour.\n\n> **Info**\n>\n> The [Mantle specification](https://lip.logos.co/blockchain/raw/bedrock-v1.1-mantle-specification.html) is the source of truth for the exact rotation algorithm. The guidance below is a practical summary.\n\n1. Set `posting_timeframe` - the length of each sequencer's turn under normal circumstances - to at least the average slots-per-block of the Logos Blockchain.\n\n   - A turn shorter than one block lets multiple sequencers become authorised within the same block, defeating the purpose of rotation.\n   - As a rule of thumb, use a multiple: with a ~20 slots/block average, `posting_timeframe = 60` gives each sequencer roughly three blocks of ownership per turn.\n\n1. Set `posting_timeout` to a value greater than or equal to `posting_timeframe`. `posting_timeout` is the time we wait for an inactive sequencer to publish before continuing to the next sequencer - all subsequent inactive sequencers have a turn of `posting_timeout` slots until one of them publishes. At that point, turns go back to being `posting_timeframe` slots long.\n\n   - Values smaller than `posting_timeframe` make the timeout branch dominate cadence as soon as the authorised sequencer goes silent.\n\n1. Confirm the worked example matches your intended behaviour before proceeding.\n\n   A channel with three accredited keys, `posting_timeframe = 60`, and `posting_timeout = 180`:\n\n   ```\n   slots 0–59     → index 0\n   slots 60–119   → index 1\n   slots 120–179  → index 2\n   slots 180–239  → index 0\n   ...\n   ```\n\n   > **Info**\n   >\n   > The Zone SDK exposes the current rotation state to your sequencer in two forms:\n   >\n   > - The full `SequencerChannelView` (`subscribe_channel_view`)—a `tokio::sync::watch` receiver carrying `authorized_key_index`, `own_key_index`, `our_turn_to_write`, the current [slot](https://docs.logos.co/get-started/glossary.md#slot), and the turn window's `turn_to_write_slots`.\n   > - A focused `TurnNotification` (`subscribe_turn_to_write`) plus `Event::TurnNotification`—emitted only when the turn boundary actually changes.\n   >\n   > `our_turn_to_write` is the boolean a sequencer reads to decide whether it is currently authorised.\n\n## Step 2: Publish inscriptions and monitor turn state\n\nThe Zone SDK queues inscriptions locally when it is not your sequencer's turn and posts them automatically when the turn arrives. You do not need to gate `publish` calls on turn state yourself.\n\n1. Call `publish` from any task at any time.\n\n   ```rust\n   // From the drive task, once `Event::Ready` has fired.\n   let (result, checkpoint) = sequencer.handle().publish(zone_block).await?;\n   // `result.tx` is a `PendingTx::Inscription(...)`.\n   // `checkpoint` is up to date with the new pending entry.\n   // The post may not have hit the node yet if it is not our turn.\n   ```\n\n   - The SDK enqueues the transaction immediately if it is your turn and posts to the network.\n   - The SDK enqueues the transaction with `posted: false` if it is not your turn; it drains automatically when the turn arrives.\n   - A periodic `resubmit_interval` (default 30 s) re-drains pending publishes if your turn is current, covering transient network failures.\n\n1. Subscribe to turn notifications to drive your UI or application logic. Choose one of the two surfaces based on your use case:\n\n   - Use `TurnNotification` (edge-triggered) to wake the UI only when the turn boundary changes:\n\n     ```rust\n     use lb_zone_sdk::sequencer::Event;\n\n     if let Event::TurnNotification { notification } = event {\n         if notification.our_turn_to_write {\n             println!(\n                 \"Our turn until slot {:?}\",\n                 notification.ends_at_slot,\n             );\n         } else {\n             println!(\n                 \"Waiting for our turn; current authorized window ends at {:?}\",\n                 notification.ends_at_slot,\n             );\n         }\n     }\n     ```\n\n   - Use `subscribe_channel_view` (snapshot) to read the full rotation state on every block, for example to render a status panel:\n\n     ```rust\n     let view_rx = sequencer.subscribe_channel_view();\n     let view = view_rx.borrow().clone();\n     if view.our_turn_to_write {\n         // Authorized right now.\n     } else if let (Some(own), Some(auth)) = (view.own_key_index, view.authorized_key_index) {\n         println!(\n             \"Sequencer {} is publishing; we are sequencer {}.\",\n             auth, own,\n         );\n     }\n     ```\n\n     The view also carries `pending_publish_txs` and `queued_messages` so the UI can show how many inscriptions are waiting for the next turn.\n\n1. Handle competing writes by listening for `Event::BlocksProcessed` events and checking `channel_update.orphaned()` for orphaned transactions. Republish these transactions with the latest parent.\n\n   > **Info**\n   >\n   > Two sequencers can race on the same parent slot during rotation transitions or after a resync. The on-chain rule is: the first valid [inscription](https://docs.logos.co/get-started/glossary.md#inscription) wins; the loser's transaction is surfaced in `channel_update.orphaned()`.\n\n   ```rust\n   use lb_zone_sdk::sequencer::{ChannelUpdateTx, Event};\n\n   if let Event::BlocksProcessed { channel_update, .. } = event {\n       for entry in channel_update.orphaned() {\n           if let ChannelUpdateTx::Inscription(info) = entry {\n               let (result, checkpoint) = sequencer.handle().publish(info.payload.clone()).await?;\n               // Persist `result` + `checkpoint` exactly as on the original publish.\n           }\n       }\n   }\n   ```\n\n   - `channel_update` is a `ChannelUpdate` object. It is a `ChannelUpdate::Extension { adopted }` when nothing you hold became invalid, and a `ChannelUpdate::Conflict { common_prefix, adopted, orphaned }` when entries left the channel view. `orphaned()` is empty on an extension.\n   - To keep your own state in step with a conflict, either revert every orphaned entry and apply every adopted one, or reset to finalised state and replay `channel_update.canonical_chain()`, the whole non-finalised view at the new tip. Your own publishes are applied at publish time and appear in `adopted` only after they were first reported orphaned.\n   - `tests/src/cucumber/steps/zone/operations/policies.rs` in the `logos-blockchain` repository contains reference policies for this republish loop: `OrphanRepublishPolicy` (simple republish), `RepublishLineagePolicy` (for repeating payloads, so each intent lands once), `SortedConflictPolicy` (republish only when it preserves the channel's sorted order), and `BalanceAwarePolicy` (republish only when the account balance still allows it).\n   - If the orphan was caused by an application-level conflict rather than a race, you can choose to drop the payload or deduplicate it against a higher-level transaction stream, or apply any other custom rule instead of republishing.\n\n   > **Note**\n   >\n   > Channel transactions built outside the publish API are classified by shape, not by how they were submitted. One inscription plus at most one funding transfer surfaces as `ChannelUpdateTx::Inscription`; with withdrawals added, as `ChannelUpdateTx::AtomicWithdraw`; with one channel transfer and no withdrawals, as `ChannelUpdateTx::PinDeposit`. A lone channel config surfaces as `ChannelUpdateTx::Config`. Anything else surfaces as `ChannelUpdateTx::Custom(SignedOps)`.\n   >\n   > For `Custom` entries the SDK hands back the whole transaction, and it is up to the consumer to parse it (the `channel_inscriptions` helper extracts its inscriptions) and decide how to recover it. The main API is `publish`, `publish_atomic_withdraw`, and `publish_pin_deposit`.\n\n   > **Info: Stuck transactions**\n   >\n   > A pending transaction that stays out of a block for longer than `SequencerConfig::stale_refund_slots` (default 30 slots of the last irreversible block, `0` disables it) is recovered automatically when the SDK can sign it alone. Transactions from `publish`, `publish_atomic_withdraw`, and `publish_pin_deposit` are re-funded, re-signed, and posted again with the same message ID. Multi-signature and custom transactions are shed instead and reported in `orphaned()`, together with everything chained on them, so you prepare them, collect signatures, and submit them again.\n\n## Step 3: Update the committee with a channel config op\n\nSubmit a channel config to add or remove sequencers from `accredited_keys`. Because a channel config overwrites the entire key list, you should re-include any keys that should remain accredited after the update.\n\n1. Use `channel_config` directly when `configuration_threshold == 1`, that is, when you only need one sequencer's signature to change the configuration.\n\n   ```rust\n   use lb_core::mantle::{\n       channel::{SlotTimeframe, SlotTimeout},\n       ops::channel::VerifiedChannelKeys,\n   };\n\n   let new_keys = VerifiedChannelKeys::from([\n       admin_pk,\n       new_sequencer_b_pk,\n       new_sequencer_c_pk,\n   ]);\n\n   let ((result, checkpoint), signed_tx) = sequencer\n       .handle()\n       .channel_config(\n           new_keys,\n           SlotTimeframe::from(60), // ~3 blocks per turn at 20 slots/block\n           SlotTimeout::from(180),  // skip after 3 turn windows of inactivity\n           1,                       // configuration_threshold (still single-admin)\n           1,                       // transfer_threshold\n       )\n       .await?;\n   ```\n\n   `channel_config` funds, signs with the local key only, and submits the transaction. On finalisation, the SDK picks up the new config and the next `BlocksProcessed` event recomputes `our_turn_to_write` for every running sequencer.\n\n1. Use the `prepare_channel_config` → `sign_with` → `submit_channel_config` flow when `configuration_threshold > 1`. In this case, collecting signatures from other sequencers is the responsibility of the application rather than the Zone SDK.\n\n   - The proposer calls `prepare_channel_config` with the new configuration. The SDK builds and funds the config transaction and returns a `PreparedChannelConfig`.\n   - Each key holder inspects `prepared.tx()` and signs with `prepared.sign_with(&key)`, which returns an `IndexedSignature` indexed against the channel's current accredited keys. A key holder without a running sequencer can call `sign_prepared(&key, &prepared.accredited_keys, &prepared.sign_payload)` instead.\n   - The proposer gathers `prepared.signing_threshold` signatures, the channel's current `configuration_threshold`, into `IndexedSignatures` and submits them with `submit_channel_config`.\n\n   ```rust\n   use lb_core::mantle::{\n       channel::{SlotTimeframe, SlotTimeout},\n       ops::channel::VerifiedChannelKeys,\n   };\n   use lb_zone_sdk::sequencer::{IndexedSignature, IndexedSignatures};\n\n   // 1. Propose the new configuration. The SDK builds and funds the tx.\n   let prepared = sequencer\n       .handle()\n       .prepare_channel_config(\n           VerifiedChannelKeys::from([admin_pk, sequencer_b_pk, sequencer_c_pk]),\n           SlotTimeframe::from(60),\n           SlotTimeout::from(180),\n           2, // configuration_threshold\n           1, // transfer_threshold\n       )\n       .await?;\n\n   // 2. Distribute `prepared` to the rest of the committee; each key holder\n   //    signs with `prepared.sign_with(&key)`. Transport is application-defined.\n   let collected: Vec<IndexedSignature> = collect_signatures_from_committee(&prepared).await?;\n\n   // 3. Gather the signatures, ordered by key index with at most one per key,\n   //    and submit.\n   let signatures = IndexedSignatures::try_from_iter(collected.into_iter().map(Into::into))?;\n   let (result, checkpoint) = sequencer\n       .handle()\n       .submit_channel_config(prepared, signatures)?;\n   ```\n\n   - A signature authorises every channel operation in the prepared transaction, not only the config, so signers should inspect the whole of `prepared.tx()` before signing.\n   - A config position holds one pending continuation, so `prepare_channel_config` refuses a second config on a parent that already has one pending until it lands or expires.\n   - An orphaned multi-signature config surfaces as `ChannelUpdateTx::Config`. Recover it by running the prepare → collect signatures → submit flow again.\n\n## Next steps\n\nFor more examples of what can be built with the Zone SDK, see the following link:\n\n- [Bridge assets between Logos Blockchain and a Zone](https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md)\n"},{"id":"blockchain/blend/join-the-blend-network-as-a-core-node","title":"Join the Blend network as a core node","summary":"Connect your blockchain node to Blend to contribute to proposer privacy.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/blend/join-the-blend-network-as-a-core-node","markdown_url":"https://docs.logos.co/blockchain/blend/join-the-blend-network-as-a-core-node.md","permalink":"/blockchain/blend/join-the-blend-network-as-a-core-node","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/blend/join-the-blend-network-as-a-core-node.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Register your node as a Blend core node"},{"depth":2,"text":"Frequently asked questions"},{"depth":3,"text":"Did my core declaration land?"},{"depth":3,"text":"Is my node participating in the Blend Network right now?"}],"markdown":"# Join the Blend Network as a core node\n\n#### Connect your blockchain node to Blend to contribute to proposer privacy.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nJoining the [Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network) lets your blockchain node contribute to the privacy of [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) proposers and receive rewards for participating. This procedure applies to operators of a running Logos Blockchain node who want to register that node as a Blend [core node](https://docs.logos.co/get-started/glossary.md#core-node). Before you start, make sure your node's address is publicly reachable so other peers can connect to it.\n\n> **Note**\n>\n> Joining the Blend Network as a core node via a [Basecamp](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-basecamp.md) is not currently supported.\n>\n> Instead, follow the CLI instructions below, but navigate to your Basecamp Logos Blockchain node's **Generate-Config** screen to find the **Blend port** field. Use this value in place of `<YOUR_BLEND_PORT>`.\n\n> **Info: Prerequisites**\n>\n>\n> - [A running blockchain node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md).\n> - A publicly reachable IP and port (or DNS) combination for the node.\n> - Stable connection (10 Mbps+ recommended) to handle multiple concurrent connections. A stable, low-latency connection is beneficial for effective message blending and random delays.\n\n## What to expect\n\n- Your blockchain node is registered as a Blend core node.\n- Your node contributes to [proposer privacy](https://docs.logos.co/get-started/glossary.md#proposer-privacy) and becomes eligible for rewards.\n- Your declaration is confirmed on-chain and becomes active after two [epochs](https://docs.logos.co/get-started/glossary.md#epoch).\n\n## Register your node as a Blend core node\n\nComplete these steps to fund the required keys, retrieve a locked [note](https://docs.logos.co/get-started/glossary.md#note), and submit your Blend join declaration.\n\n1.  Start the node and poll until the mode switches to `\"Online\"`. This takes approximately one hour:\n\n    ```bash\n    logosctl call blockchain_module get_cryptarchia_info | jq -r .result.value | jq .mode\n    # > \"Bootstrapping\"\n\n    # ... Wait ~1h, then re-run until you see:\n    # > \"Online\"\n    ```\n\n1.  Open your `keystore.yaml` and note the `BlendZk` and `SdpFunding` public keys. Both need funds.\n\n    ```yaml\n    # keystore.yaml\n    public_keys:\n      ...\n      BlendZk: 13cccf99f90fd78c2134891ce3c1afce0605753a7694b9d56678d63a8d471820\n      ...\n      SdpFunding: 91d381a87e05d46fc9bc95246273b6930290506f0589ad039444decd3c24940e\n      ...\n    ```\n\n1.  Fund both keys by mining. Start mining. Auto-claim pays the mined rewards into your node's `PoWClaim` key, and you then transfer funds from that key to the `BlendZk` and `SdpFunding` keys:\n\n    ```bash\n    logosctl call blockchain_module pow_start_mining\n    ```\n\n    - The `BlendZk` key needs a single note of at least 1,000,000,000 (1 billion), the minimum stake that the declaration locks.\n    - The `SdpFunding` key pays the fees for the declaration and for the activity message your node sends each epoch. Send it at least 50,000,000 (50 million), which covers two months of fees with a wide margin for fee increases.\n\n1.  Check the auto-claim target. Its `public_key` is your `PoWClaim` key, and its `balance` grows as auto-claim collects rewards. Rerun this command periodically until the balance is at least 1,050,000,000 (1.05 billion):\n\n    ```bash\n    logosctl call blockchain_module pow_status | jq -r .result.value | jq .auto_claim.targets\n    ```\n\n    - With the generated config, auto-claim pays the `PoWClaim` key up to 2,000,000,000 (2 billion), then stops itself and stops mining. You can continue as soon as the balance reaches 1.05 billion, or wait for mining to stop at 2 billion.\n\n    > **Info: How long mining takes**\n    >\n    > Each claim pays about 2 million, so 1.05 billion takes about 525 claims and 2 billion about 1,000. How fast a node collects claims depends on its CPU and on how many other nodes are mining. On Testnet v0.3.1 in October 2026, a typical node collected about 8 claims an hour, so it reached 1.05 billion in about 3 days and 2 billion in about 5.5 days. The fastest nodes collected about 65 claims an hour and reached 2 billion in about 15 hours. To estimate your own time, check the `balance` an hour apart.\n\n1.  Merge the mined rewards into larger notes. Auto-claim pays each claim as a separate note of about 2 million, but a transfer can spend at most 32 notes, so the 1 billion transfer can't be sent from these small notes directly. This script repeatedly transfers the 32 largest notes of the `PoWClaim` key back to itself as one note, waiting for each transfer to land, until the 32 largest notes together cover the 1 billion transfer plus a margin for its fee. Replace `<POW_CLAIM_KEY>` with the `public_key` from the previous step:\n\n    ```bash\n    K=<POW_CLAIM_KEY>; n() { logosctl call blockchain_module wallet_get_notes $K \"\" | jq -r .result.value | jq \"[.notes[].value | tonumber] | sort | reverse | $1\"; }\n    if [ $(n 'add // 0') -lt 1000100000 ]; then echo \"Not enough funds: the key holds $(n 'add // 0'), and merging needs at least 1000100000. Keep mining.\"; else\n    while [ $(n length) -gt 2 ] && [ $(n '.[:32] | add') -lt 1000100000 ]; do a=$(( $(n '.[:32] | add') - 100000 )); logosctl call blockchain_module wallet_transfer_funds $K \"json:[\\\"$K\\\"]\" $K str:$a \"\" | jq -e .result.success || break; until [ $(n '.[0]') -ge $a ]; do sleep 30; done; done; fi\n    ```\n\n    - If the key holds less than 1,000,100,000 in total, the script prints the current balance and stops without sending anything. Keep mining and run it again later.\n\n    - Each round merges up to 32 notes and needs a block to land, so reaching 1 billion takes about 16 rounds. Expect this to take 15 to 30 minutes.\n    - Each round pays a fee of about 1,800 at current gas prices.\n\n1.  Transfer 1,000,000,000 (1 billion) from your `PoWClaim` key to the `BlendZk` key, replacing the placeholders with your keys. The `PoWClaim` key pays the fee and receives any change:\n\n    ```bash\n    logosctl call blockchain_module wallet_transfer_funds \\\n      <POW_CLAIM_KEY> 'json:[\"<POW_CLAIM_KEY>\"]' <BLEND_ZK_KEY> str:1000000000 \"\"\n\n    # A successful call returns the transaction hash:\n    # > {\"method\":\"wallet_transfer_funds\",\"module\":\"blockchain_module\",\"result\":{\"error\":null,\"success\":true,\"value\":\"<TX_HASH>\"},\"status\":\"ok\"}\n    ```\n\n    - The arguments are, in order: the key that receives any change, the list of keys whose notes pay for the transfer, the recipient key, the amount, and the block to build the transfer on, where `\"\"` means the current tip.\n    - The `json:` prefix passes the funding keys as a list, and the `str:` prefix passes the amount as text, which `wallet_transfer_funds` requires. Both prefixes need `logosctl` 0.3.2 or later.\n\n1.  Once the 1 billion transfer has landed, which you can check with `wallet_get_notes` as shown in the next step, transfer the remaining funds to the `SdpFunding` key. Send at least 50,000,000 (50 million):\n\n    ```bash\n    logosctl call blockchain_module wallet_transfer_funds \\\n      <POW_CLAIM_KEY> 'json:[\"<POW_CLAIM_KEY>\"]' <SDP_FUNDING_KEY> str:50000000 \"\"\n    ```\n\n    - One transfer can move about 60 million at most, because it spends at most 32 notes. To move more of the remaining balance, repeat the transfer once the previous one has landed.\n\n1.  Wait until both keys have received funds. Check each balance with `wallet_get_notes`:\n\n    ```bash\n    logosctl call blockchain_module wallet_get_notes <ADDRESS> \"\" \\\n      | jq -r .result.value | jq .notes\n    # > [\n    # >   {\n    # >     \"id\": \"<BLEND_ZK_NOTE_ID>\",\n    # >     \"value\": \"1000000000\"\n    # >   }\n    # > ]\n    ```\n\n    - Note the `id` of the 1 billion note held by the `BlendZk` key—you need it in the next step.\n\n1.  Join the Blend Network by locking the 1 billion note held by your `BlendZk` key.\n\n    > **Info**\n    >\n    > Make sure to open `<YOUR_BLEND_PORT>/udp` on the public host firewall before running the following command. `<YOUR_BLEND_PORT>` can be found in `user_config.yaml` under `blend.core.backend.listening_address`. Configure the firewall and NAT forwarding before joining and verify the local listener and public reachability after activation.\n\n    ```sh\n    logosctl call blockchain_module blend_join_as_core_node \\\n       \"/ip4/<YOUR_IP>/udp/<YOUR_BLEND_PORT>/quic-v1\" \\\n       \"<BLEND_ZK_NOTE_ID>\"\n\n    # A successful call will return the declaration id:\n    # > {\"method\":\"blend_join_as_core_node\",\"module\":\"blockchain_module\",\"result\":{\"error\":null,\"success\":true,\"value\":\"2691821bd61394cc18939626de4e9231c699e8ddefd1ebf9e6c35b32229bdc65\"},\"status\":\"ok\"}\n    ```\n\n    - `<YOUR_IP>`: Must be your external IP address\n    - `<YOUR_BLEND_PORT>`: Your configured Blend port from the `user_config.yaml` file (`blend.core.backend.listening_address`). Note that if you do port-mapping, the external mapped port must be used.\n    - `<BLEND_ZK_NOTE_ID>`: The ID of the 1 billion note held by your `BlendZk` key, as queried above.\n    - The Blend core listener starts only after the node's declaration becomes active.\n\n1.  Confirm the declaration was accepted on-chain by polling `/mantle/sdp/declarations` and looking for your entry:\n\n    ```bash\n    curl http://<YOUR_NODE_IP>:8080/mantle/sdp/declarations | jq .\n    # > {\n    # >   \"<DECLARATION_ID>\": {\n    # >     \"service_type\": \"BN\",\n    # >     \"provider_id\": \"35d60d973560b8344f83dc266a3fe89e35a3dcf9959c492d0a7a0b7a85c5d2ce\",\n    # >     \"locked_note_id\": \"<BLEND_ZK_NOTE_ID>\",\n    # >     \"locators\": [\n    # >       \"/ip4/<YOUR_IP>/udp/<YOUR_BLEND_PORT>/quic-v1\"\n    # >     ],\n    # >     \"zk_id\": \"13cccf99f90fd78c2134891ce3c1afce0605753a7694b9d56678d63a8d471820\",\n    # >     \"created\": 1,\n    # >     \"active\": 3,\n    # >     \"withdraw_at\": null,\n    # >     \"nonce\": 0\n    # >   }\n    # > }\n    ```\n\n    - The response is a JSON **object keyed by declaration id** (not a list). Find your entry by its `provider_id` (your `BlendSigning` key) or `zk_id` (your `BlendZk` key).\n    - `service_type: \"BN\"` identifies this as a [Blend node](https://docs.logos.co/get-started/glossary.md#blend-node) declaration.\n    - `created` is the epoch your declaration was included; it takes effect about two epochs later. `active` is the most recent epoch your node has re-attested activity for (via the periodic Active message), so it **advances over time**—it equals `created + 2` right after activation and grows on a long-running node.\n    - If your declaration is not yet listed, retry after your transaction is included in a block.\n\n1. Check the configured Blend UDP listener:\n\n   ```sh\n   ss -lun\n   ```\n\n   - Confirm that the local UDP port from `blend.core.backend.listening_address` is present. If the public Blend port differs, also confirm that NAT forwards `<YOUR_BLEND_PORT>/udp` to this local port.\n\n## Frequently asked questions\n\n### Did my core declaration land?\n\nAfter declaring as a core node, query the on-chain SDP declarations and look for your entry:\n\n```bash\ncurl -s http://localhost:8080/mantle/sdp/declarations\n```\n\nFind the entry whose `zk_id` is your BlendZk key, with `\"service_type\": \"BN\"`. It will become active a couple of epochs after the declaring transaction is included in a block, and stays active only while the node keeps sending Active messages. \n\n### Is my node participating in the Blend Network right now?\n\nCheck the node log for the Blend [Service](https://docs.logos.co/get-started/glossary.md#service) lifecycle:\n\n```bash\ngrep -aE \"blend::service\" <node-log> | tail\n```\n\n- `Waiting for chain to become Online mode`—not yet; the node is still [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping).\n- `Chain is now Online`, followed by the Blend Service starting and `Blend edge swarm started with local peer id …`—proposals will be routed through Blend, whether your node is a core node or not.\n- `current membership is ready members=N`—the node sees `N` active core nodes this epoch. If `N` is below the minimum needed, the node falls back to [broadcast mode](https://docs.logos.co/blockchain/concepts/about-the-blend-network.md#node-roles-core-edge-and-broadcast) for that epoch (no Blend privacy).\n\nThere is also an API endpoint, `curl http://localhost:8080/blend/info`, which returns the Blend Network info once the node is Online. Note that it can hang or time out while the node is still bootstrapping (Blend is not up yet), so it is better to use the log check during sync.\n"},{"id":"blockchain/logos-sql/introduction-to-logos-sql","title":"Introduction to Logos SQL","summary":"Learn how applications share a database through the Logos Blockchain while keeping a local copy of their data.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/logos-sql","markdown_url":"https://docs.logos.co/blockchain/logos-sql.md","permalink":"/blockchain/logos-sql","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/logos-sql/introduction-to-logos-sql.md","headings":[{"depth":2,"text":"State and history"},{"depth":2,"text":"Channels define databases"},{"depth":2,"text":"Writes start locally"},{"depth":2,"text":"Latest vs finalised state"},{"depth":2,"text":"Responding to changes in history"},{"depth":2,"text":"Bootstrapping and restarts"},{"depth":2,"text":"Before building"}],"markdown":"# Introduction to Logos SQL\n\n#### Learn how applications share a database through the Logos Blockchain while keeping a local copy of their data.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nLogos SQL is a Rust library that gives an application a local SQLite database and synchronises its writes with other participants through the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain). Applications can create tables, submit SQL transactions and query their own database, with the Logos SQL library handling replication.\n\nLogos SQL is useful for participants who need shared data without relying on one organisation to host the database for everyone. Each participant maintains its own copy. Different applications can use the same data by following the same channel.\n\n## State and history\n\nThere are two ways to describe a database: the data it contains now, and the writes that produced it. Logos SQL connects these two views:\n\n- **Local SQLite holds the current data.** The application can query it without making a network request for each read.\n- **Logos Blockchain carries the shared write history.** It determines the accepted writes and their order. Each participant applies that history to its own database.\n\nThe blockchain does not execute the SQL. Logos SQL runs it on each participant's machine and uses the [Zone SDK](https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md) to publish and receive the shared history.\n\n```text\nApplication A --> Logos SQL A <--> Local SQLite A\n                       |\n                       |\n              Shared channel history\n                 Logos Blockchain\n                       |\n                       |\nApplication B --> Logos SQL B <--> Local SQLite B\n```\n\nBoth library instances follow the channel. Instances configured as writers can also publish to it.\n\n## Channels define databases\n\nA [channel](https://docs.logos.co/get-started/glossary.md#channel) is an ordered history of [inscriptions](https://docs.logos.co/get-started/glossary.md#inscription) on the Logos Blockchain. Logos SQL uses these inscriptions to share SQL transactions between participants.\n\nInstances using the same channel follow the same database history, while different channels have separate histories. The channel carries both schema changes, such as creating tables, as well as writes to the data in those tables.\n\nA writer, as a sequencer for the database's channel, needs an authorised signing key and funding for inscription fees, while a read-only instance follows the history without publishing. Channel permission controls who can submit writes; it does not provide separate SQL permissions for individual tables or rows.\n\n## Writes start locally\n\nAn application prepares SQL statements and bound parameters, then calls `execute`.\n\n1. Logos SQL runs the transaction against the local database. Its statements commit together or roll back together.\n2. After saving the local changes and recovery information, it returns a transaction ID. The application can already read its changes.\n3. The library publishes the write in the background. Other participants receive the channel history and apply it locally.\n4. When the chain finalises that history, the write becomes part of the finalised database.\n\nA successful `execute` means **committed locally**, not **finalised on the chain**. This keeps the application responsive while publication and agreement happen over the network.\n\nAfter startup, applications can continue writing locally during a connection outage while there is room in the pending-write queue. If the queue fills, further writes return `PublishPending` without committing their changes. Saved writes remain available for publication when the connection returns, but changes in channel history can displace them. Starting an instance still requires a connection to catch up with channel history.\n\n## Latest vs finalised state\n\nLogos SQL provides two local read connections:\n\n| View | Contents | Use it for |\n| --- | --- | --- |\n| Latest | The most up-to-date local state, including writes that are not yet finalised. | Showing and working with recent changes. |\n| Finalised | The database produced by finalised channel history. | Decisions that must use settled history. |\n\nThe latest state can include changes that are undone when the chain switches branches. Finalised history is not undone by those reorganisations, though later finalised writes can still update its rows.\n\nParticipants can temporarily see different data while writes are pending or they are catching up. Applying the same accepted history brings their replicas into agreement.\n\n## Responding to changes in history\n\nTwo writers could end up competing for the next inscription in the channel, or a chain reorganisation can remove previously accepted writes. A local write may then lose its place in history even though its SQL was successfully executed.\n\nLogos SQL updates the live database to match the current history. It reports affected local writes as **displaced** and blocks ordinary new writes until the application has considered them. The application can retry the original SQL, continue without retrying it, or prepare a different change.\n\nThe library does not automatically decide which action is appropriate. An old update might overwrite a newer value if repeated. A displaced write can also return if its branch becomes canonical again.\n\nSee [Handle displaced writes](https://docs.logos.co/blockchain/logos-sql/handle-displaced-writes.md) for the application flow and examples of SQL conditions.\n\n## Bootstrapping and restarts\n\nA new participant can build its database from existing channel history via `LogosSql::start`. Another application instance does not have to prepare and send a database export.\n\nEach participant keeps a separate persistent state directory. On restart, it resumes using that directory. Replication runs in the background for as long as the Logos SQL library is running.\n\n## Before building\n\n- **Protect sensitive data before submission.** Logos SQL does not encrypt application SQL or parameter values. Deleting a row does not erase the write from chain history.\n- **Submit writes through Logos SQL.** Writes must go through the Logos SQL API to be shared with other participants. Changing the SQLite database directly does not publish those changes.\n- **Plan for provisional results.** Use finalised reads where needed, and decide how the application responds to displaced writes.\n\n> **Tip**\n>\n> Follow [Build an application with Logos SQL](https://docs.logos.co/blockchain/logos-sql/build-application-with-logos-sql.md) to run the password-manager example, follow its writes between participants and review displaced writes.\n"},{"id":"blockchain/logos-sql/build-application-with-logos-sql","title":"Build an application with Logos SQL","summary":"Run a password-manager example and follow its SQL writes between two local databases.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/logos-sql/build-application-with-logos-sql","markdown_url":"https://docs.logos.co/blockchain/logos-sql/build-application-with-logos-sql.md","permalink":"/blockchain/logos-sql/build-application-with-logos-sql","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/logos-sql/build-application-with-logos-sql.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Build the example"},{"depth":2,"text":"Step 2: Configure and start the application"},{"depth":3,"text":"Understand how it works"},{"depth":2,"text":"Step 3: Add and read a credential"},{"depth":3,"text":"Understand how it works"},{"depth":2,"text":"Step 4: Read from another participant"},{"depth":2,"text":"Step 5: Update the password and notes"},{"depth":3,"text":"Understand how it works"},{"depth":2,"text":"Step 6: Review a displaced write"},{"depth":2,"text":"Step 7: Remove the entry and stop"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"Startup prints nothing and does not return"},{"depth":3,"text":"A credential cannot be decrypted"},{"depth":3,"text":"Adding a credential reports a primary key constraint error"},{"depth":3,"text":"The second instance does not show a change"},{"depth":3,"text":"A request returns PublishPending"},{"depth":3,"text":"A read needs finalised data"}],"markdown":"# Build an application with Logos SQL\n\n#### Run a password-manager example and follow its SQL writes between two local databases.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis guide demonstrates how Logos SQL can be used to maintain an application's database on the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain). It does this by walking through a [password-manager example](https://github.com/logos-blockchain/logos-blockchain/tree/47a65ab3c/logos_sql/examples/password_manager) in the `logos-blockchain` repository. It shows how an application starts Logos SQL, prepares SQL transactions and reads its local database.\n\nThe example separates startup configuration, database operations and terminal commands. The same library calls apply to other applications. Read the [introduction](https://docs.logos.co/blockchain/logos-sql.md) first for the relationship between local data and shared history.\n\n> **Danger: Use fake credentials only**\n>\n> The example encrypts accounts, passwords and notes before submitting them to Logos SQL. Labels and write activity remain public. It is a demonstration, not an audited password manager: use fake credentials only. Deleting an entry does not erase its encrypted data from channel history.\n\nThe walkthrough uses two instances on your own machine, each with a separate database directory.\n\n> **Info: Prerequisites**\n>\n>\n> - Git, Rust and the [Logos Blockchain build prerequisites](https://github.com/logos-blockchain/logos-blockchain/blob/master/README.md#prerequisites).\n> - The OpenSSL command-line tool to generate keys for the example.\n> - A compatible Logos Blockchain node that has reached `Online`, with a reachable HTTP API (default: `http://127.0.0.1:8080`). Follow [Run a Logos Blockchain node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) to set one up.\n> - A funded key in the node's wallet for [inscription](https://docs.logos.co/get-started/glossary.md#inscription) fees. On a testnet node, follow [Mine tokens to fund your node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md#step-4-mine-tokens-to-fund-your-node) to fund the `PoWClaim` key. A local standalone node configures no auto-claim targets, so use the funded genesis key from its `wallet.known_keys` instead, as [Step 2](#step-2-configure-and-start-the-application) sets out.\n> - A maximum inscription fee appropriate for your test network.\n>\n\n## What to expect\n\n- You can build and run an existing Logos SQL application.\n- You can update a fake credential and read it from another participant.\n- You can identify the SQL calls and review commands used to handle displaced writes.\n\n## Step 1: Build the example\n\nLogos SQL is not yet published to crates.io. Build the example inside its source workspace.\n\n1. Clone the repository and check out the tested revision:\n\n   ```bash\n   git clone https://github.com/logos-blockchain/logos-blockchain.git\n   cd logos-blockchain\n   git checkout 47a65ab3c\n   ```\n\n1. Build the password manager:\n\n   ```bash\n   cargo build --locked --release -p logos-sql --example password_manager\n   ```\n\n1. Check its startup options:\n\n   ```bash\n   ./target/release/examples/password_manager --help\n   ```\n\n   **Expected result:** help lists the encryption key file, channel, node, signing key, funding key, fee and state-directory options.\n\n## Step 2: Configure and start the application\n\n1. In terminal A, generate a separate encryption key for this example:\n\n   ```bash\n   (umask 077; openssl rand -out password-manager.key 32)\n   ```\n\n   Run this once, before starting either instance. Do not overwrite an existing key. The file contains 32 random bytes, not a password or a hexadecimal string. Keep it private and backed up: without it, the encrypted data cannot be read. Do not use the channel signing key for encryption.\n\n1. Generate a channel signing key:\n\n   ```bash\n   (umask 077; openssl genpkey -algorithm ed25519 -out channel-signing.pem)\n   ```\n\n   Generate this key only once and keep the file private. Do not overwrite it when restarting or adding a participant. Its public key identifies the new [channel](https://docs.logos.co/get-started/glossary.md#channel); you do not need to create the channel separately.\n\n1. Extract the signing key and channel ID in the hexadecimal format the example accepts:\n\n   ```bash\n   export LOGOS_SQL_SIGNING_KEY=$(openssl pkey -in channel-signing.pem -outform DER | tail -c 32 | od -An -tx1 | tr -d ' \\n')\n   export LOGOS_SQL_CHANNEL_ID=$(openssl pkey -in channel-signing.pem -pubout -outform DER | tail -c 32 | od -An -tx1 | tr -d ' \\n')\n   ```\n\n   These are 32-byte values without a `0x` prefix. See the [Zone SDK channel setup](https://docs.logos.co/blockchain/zone-sdk/inscribe-data-on-chain-using-zone-sdk.md#step-2-initialise-the-zonesequencer-struct) for channel identity and signing authority.\n\n1. Set the remaining values from your test-network setup:\n\n   ```bash\n   export LOGOS_SQL_NODE_URL='<node-http-url>'\n   export LOGOS_SQL_FUNDING_KEY='<funding-public-key-hex>'\n   export LOGOS_SQL_MAX_TX_FEE='<maximum-fee>'\n   export LOGOS_SQL_ENCRYPTION_KEY_FILE=\"$PWD/password-manager.key\"\n   ```\n\n   Replace every angle-bracket placeholder:\n   - For a local node, the default HTTP URL is `http://127.0.0.1:8080`.\n   - On a testnet node, use the auto-claim target's `public_key` as the funding key: `pow_status` prints it in [Step 4 of the \"Run a Blockchain node\" tutorial](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md#step-4-mine-tokens-to-fund-your-node), and it is also at `pow.auto_claim.targets[0].public_key` in the node's `user_config.yaml`.\n   - On a local standalone node, which configures no auto-claim targets, use a funded key from the node's `wallet.known_keys`—check a candidate with `curl -s http://127.0.0.1:8080/wallet/<key>/balance` first, because the genesis keys are not all funded. The example does not create or fund a wallet.\n\n   > **Note**\n   >\n   > This walkthrough was tested with example source at commit `47a65ab3c` and `LOGOS_SQL_MAX_TX_FEE=1000000`. This value is a fee cap, not a fixed charge. Required fees can change with network conditions.\n\n1. Start the application with its own state directory:\n\n   ```bash\n   ./target/release/examples/password_manager --state-dir ./password-manager-a\n   ```\n\n   **Expected result:** after catching up and preparing the schema, it prints:\n\n   ```text\n   Password manager is running. Enter `help` to list commands.\n   Credentials are encrypted; labels remain public. Use test credentials only.\n   password-manager>\n   ```\n\n### Understand how it works  \n\n1. Open `logos_sql/examples/password_manager/passwords.rs` and find `PasswordManager::start`:\n\n   ```rust\n   /// Starts `λSQL`, catches up with the channel, and prepares the database.\n   pub async fn start(config: LogosSqlConfig, encryption: Encryption) -> AppResult<Self> {\n      let manager = Self {\n         logos_sql: LogosSql::start(config).await?,\n         encryption,\n      };\n\n      match manager.initialize().await {\n         // Keep the terminal available if schema creation was displaced.\n         Ok(()) | Err(AppError::LogosSql(LogosSqlError::UnhandledDisplacements)) => {}\n         Err(error) => {\n               manager.shutdown().await?;\n               return Err(error);\n         }\n      }\n\n      Ok(manager)\n   }\n   ```\n\n   It calls `LogosSql::start(config).await?`, then checks whether the `credentials` table exists. If needed, it submits `CREATE TABLE` through Logos SQL. A later participant obtains the schema by replaying channel history.\n\n1. Then, open `config.rs`. This file handles constructing `LogosSqlConfig`, with signing and funding credentials in `WriterConfig`:\n\n   ```rust\n   /// Parses startup options and builds the `λSQL` configuration.\n   pub fn from_args() -> AppResult<(LogosSqlConfig, Encryption)> {\n      let options = Options::parse();\n      let encryption =\n         Encryption::from_file(&options.encryption_key_file, options.channel_id.as_ref())?;\n\n      let config = LogosSqlConfig {\n         channel_id: options.channel_id,\n         node_url: options.node_url,\n         writer: Some(WriterConfig {\n               publication: PublicationConfig::default(),\n               signing_key: options.signing_key,\n               funding: FundingConfig {\n                  funding_pk: options.funding_key,\n                  change_pk: None,\n                  max_tx_fee: options.max_tx_fee.into(),\n                  priority_fee_percent: options.priority_fee_percent,\n               },\n         }),\n         state_dir: options.state_dir,\n      };\n\n      Ok((config, encryption))\n   }\n   ```   \n   \n   Publication settings control batching and queue limits. The library drives replication in the background; the application does not run a separate [Zone SDK](https://docs.logos.co/get-started/glossary.md#zone-sdk) event loop.\n\n   > **Note**\n   >\n   > Encryption belongs to the application, not `LogosSqlConfig`. The example uses XChaCha20-Poly1305 from RustCrypto to encrypt each field and detect tampering. It stores the nonce with the encrypted value; the key stays in the local key file and is never submitted to Logos SQL.\n\n## Step 3: Add and read a credential\n\n1. At the prompt in terminal A, enter:\n\n   ```text\n   add email demo@example.org not-a-real-password\n   ```\n\n   **Expected result:** `committed locally as <tx-id>; not final`. The transaction ID varies. This confirms a local commit, not publication or finality.\n\n1. Read the credential:\n\n   ```text\n   show email\n   ```\n\n   **Expected result:**\n\n   ```text\n   email\n     account: demo@example.org\n     password: not-a-real-password\n     notes:\n   ```\n\n### Understand how it works\n\n1. Find `prepare_credential_insert` in `passwords.rs`:\n\n   ```rust\n   fn prepare_credential_insert(credential: &StoredCredential) -> TransactionBuilder {\n       TransactionBuilder::new(\n           \"INSERT INTO credentials (label, account, password, notes)\n            VALUES (?1, ?2, ?3, ?4)\",\n       )\n       .bind(&credential.label)\n       .bind(credential.account.as_slice())\n       .bind(credential.password.as_slice())\n       .bind(credential.notes.as_slice())\n   }\n   ```\n\n1. In the same file, find the application's `add` method. It encrypts the account, password and initially empty notes into a `StoredCredential`:\n   \n   ```rust\n   /// Adds one credential.\n   ///\n   /// Concurrent attempts to use the same label produce a primary-key\n   /// conflict.\n   pub async fn add(&self, label: String, account: String, password: String) -> AppResult<TxId> {\n      let credential = StoredCredential {\n         account: self.encryption.encrypt(&label, \"account\", &account)?,\n         password: self.encryption.encrypt(&label, \"password\", &password)?,\n         notes: self.encryption.encrypt(&label, \"notes\", \"\")?,\n         label,\n      };\n\n      Ok(self\n         .logos_sql\n         .execute(prepare_credential_insert(&credential))\n         .await?)\n   }\n   ```\n\n   Each `bind` above supplies the next parameter through the standard `rusqlite::ToSql` interface: a readable label and three encrypted binary values. The app then passes this builder to `self.logos_sql.execute(...).await?`.\n\n   > **Tip**\n   >\n   > For multiple statements in one SQL transaction, append `.query(sql)` and its bindings. They commit or roll back together under one transaction ID. The library owns `BEGIN` and `COMMIT`; do not include them in the SQL.\n\n1. Find `credential` in the same file:\n\n   ```rust\n   /// Reads one credential from the local `SQLite` database.\n   pub fn credential(&self, label: &str) -> AppResult<Option<Credential>> {\n      self.stored_credential(label)?\n         .map(|credential| credential.decrypt(&self.encryption))\n         .transpose()\n   }\n\n   fn stored_credential(&self, label: &str) -> AppResult<Option<StoredCredential>> {\n      if !self.schema_exists()? {\n         return Ok(None);\n      }\n\n      let connection = self.logos_sql.read_connection()?;\n\n      let mut statement = connection.prepare(\n         \"SELECT label, account, password, notes\n            FROM credentials WHERE label = ?1\",\n      )?;\n\n      Ok(statement\n         .query_row([label], |row| {\n               Ok(StoredCredential {\n                  label: row.get(\"label\")?,\n                  account: row.get(\"account\")?,\n                  password: row.get(\"password\")?,\n                  notes: row.get(\"notes\")?,\n               })\n         })\n         .optional()?)\n   }\n   ```\n\n It calls `stored_credential`, which opens `self.logos_sql.read_connection()?` and queries `credentials` with ordinary SQLite calls. The app decrypts the fields before displaying them. Reads use the local database, not a network request.\n\n## Step 4: Read from another participant\n\n1. In terminal B, enter the same source checkout and repeat only the `export` commands from [Step 2](#step-2-configure-and-start-the-application). They reuse the existing `channel-signing.pem` and `password-manager.key` files. Do not generate new keys: both instances must use the same channel and encryption key. On separate machines, each instance needs a securely transferred copy of the encryption key.\n\n   The example has no read-only startup mode: both instances have writer credentials, even when you only issue read commands. Keep these test credentials on your own machine.\n\n1. Start a second instance with a different directory:\n\n   ```bash\n   ./target/release/examples/password_manager --state-dir ./password-manager-b\n   ```\n\n1. At its prompt, enter `show email`.\n\n   **Expected result:** the same account and fake password appear. If it prints `credential not found`, leave both processes running and try again after the write reaches this participant.\n\n   Each instance has its own SQLite database. The schema and data arrive through channel history; no database files are copied between them. Seeing the row here confirms replication, not finality.\n\n## Step 5: Update the password and notes\n\n1. In terminal A, enter:\n\n   ```text\n   update email another-fake-password\n   notes email recovery codes stored separately\n   ```\n\n   **Expected result:** each command reports `committed locally as <tx-id>; not final`.\n\n1. In terminal B, run `show email` again until both changes appear. Run `list` to see the label and account:\n\n   ```text\n   email (demo@example.org)\n   ```\n\n### Understand how it works\n\n1. Find `prepare_password_update` in `passwords.rs`:\n\n   ```rust\n   fn prepare_password_update(label: &str, password: &[u8], old_password: &[u8]) -> TransactionBuilder {\n       TransactionBuilder::new(\n           \"UPDATE credentials SET password = ?2\n            WHERE label = ?1 AND password = ?3\",\n       )\n       .bind(label)\n       .bind(password)\n       .bind(old_password)\n   }\n   ```\n\n   The application reads the stored password, checks it can decrypt it, and encrypts the replacement. The SQL compares the original encrypted bytes with the bytes still in the row; it does not decrypt them. Notes updates check the old notes independently, so changing the notes does not invalidate the password condition. Deletes check all the entry's stored values as read when the command was prepared.\n\n   These conditions stay with the original SQL if the user retries a displaced write. The retry reuses the saved encrypted values, rather than encrypting them again. A condition that no longer matches changes no rows; SQLite still considers that successful. Check the resulting data rather than treating a transaction ID as proof that a value changed.\n\n## Step 6: Review a displaced write\n\nThis step applies if channel history changes and the application reports displaced writes. The commands above do not deliberately create a fork, so an ordinary run may have none.\n\n1. Enter `displacements` to list writes awaiting review. If there are none, it prints `no displacements awaiting review`.\n\n   While any remain, ordinary new writes are blocked. A blocked write prints an error and directs the user to `displacements`, then `show` or `list` to inspect the data.\n\n1. Inspect the current entry with `show email`. Decide whether the original operation should still be attempted.\n\n1. Choose one action for an ID from the list, replacing `<tx-id>`:\n\n   | Command | Effect |\n   | --- | --- |\n   | `retry <tx-id>` | Executes the saved SQL with its original conditions and marks that displacement handled after success. |\n   | `handle <tx-id>` | Marks the displacement handled without submitting another write. |\n\n   A retry prints `retry committed locally as <new-tx-id>; not final`. Use `show` to check the data afterwards. If the selected displacement has changed since it was listed, refresh `displacements` and review it again.\n\n   This example leaves retries to the user. Other applications can handle displacements automatically when their SQL conditions and application policy make that safe. Handling a displacement does not cancel the original write; it can return after another reorganisation.\n\n1. Check `displacements` again before continuing. If another displacement arrives during review, the next ordinary write remains blocked until that item is handled too.\n\n   See [Handle displaced writes](https://docs.logos.co/blockchain/logos-sql/handle-displaced-writes.md) for the underlying library calls and how to add this flow to another application.\n\n## Step 7: Remove the entry and stop\n\n1. In terminal A, enter `remove email`.\n\n1. In terminal B, run `show email` until it prints `credential not found`.\n\n   If the entry keeps appearing, run `displacements` in terminal A. If the removal is listed, review it as described in [Step 6](#step-6-review-a-displaced-write).\n\n1. Enter `exit` in both terminals. The example calls `shutdown().await` to stop replication. Shutdown does not wait for finality.\n\n1. Restart either instance with its original command and state directory, then enter `list`.\n\n   **Expected result:** it resumes from saved state and follows the channel. If the deletion remains in the current history, `email` is absent. A chain reorganisation can still change non-finalised data.\n\n## Troubleshooting\n\n### Startup prints nothing and does not return\n\n`LogosSql::start` processes channel history before it returns, so the example is silent while it catches up—a few minutes is normal on a local node, even when everything is configured correctly. If the prompt has not appeared after that, check that the node is running, that it reports `mode: Online`, and that `LOGOS_SQL_NODE_URL` matches its `api.backend.listen_address`. The state directory is created before the first connection attempt, so finding database files there does not mean startup succeeded.\n\n### A credential cannot be decrypted\n\nCheck that both instances use the same encryption key file contents and channel. A wrong key or modified encrypted data causes decryption to fail. Do not generate a replacement key to fix this: a new key cannot decrypt existing entries.\n\nUse a fresh channel and state directory for this encrypted example. It does not migrate old unencrypted entries or rotate keys. Encrypting future writes cannot hide unencrypted data already published in the channel history.\n\n### Adding a credential reports a primary key constraint error\n\nThe label already exists. Use `show` to inspect it, `update` to change its password, or choose another label for a new entry.\n\n### The second instance does not show a change\n\nCheck that both instances follow the same channel, that the node is processing blocks, and that the writer has valid signing authority and funding. Keep both processes running and issue the read command again. The terminal does not refresh displayed results automatically.\n\n### A request returns PublishPending\n\nThe pending-write queue is full or the library is still saving an accepted publication. The new request has not committed any SQL changes. Leave replication running and try that request later; do not assume other errors have the same retry guarantee.\n\n### A read needs finalised data\n\nThe example reads the latest local state. Applications needing settled history can use `finalized_read_connection()` with the same SQLite API. The schema and each data change appear in that view only after their writes finalise.\n"},{"id":"blockchain/logos-sql/handle-displaced-writes","title":"Handle displaced writes","summary":"Find out why a Logos SQL write was blocked, review displaced writes and decide how to continue.","section":"blockchain","doc_type":"procedure","url":"https://docs.logos.co/blockchain/logos-sql/handle-displaced-writes","markdown_url":"https://docs.logos.co/blockchain/logos-sql/handle-displaced-writes.md","permalink":"/blockchain/logos-sql/handle-displaced-writes","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/logos-sql/handle-displaced-writes.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Recognise a blocked write"},{"depth":2,"text":"Step 2: Find out what was displaced"},{"depth":2,"text":"Step 3: Retry or continue without the write"},{"depth":3,"text":"Option A—Retry the original SQL"},{"depth":3,"text":"Option B—Continue without retrying the original SQL"},{"depth":2,"text":"Step 4: Continue writing"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"A reviewed item returns StaleDisplacement"},{"depth":3,"text":"Writes remain blocked after review"},{"depth":3,"text":"A handled write becomes live again"}],"markdown":"# Handle displaced writes\n\n#### Find out why a Logos SQL write was blocked, review displaced writes and decide how to continue.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nA write can succeed in your local database but later be removed: another writer may publish before it, or a blockchain reorganisation may replace the branch containing it. Logos SQL reports that write as displaced and blocks new writes until the application has reviewed it.\n\nReview does not have to involve a user. An application can automatically retry writes designed to remain safe after other changes, or mark them handled if they are no longer needed. The [password-manager example](https://docs.logos.co/blockchain/logos-sql/build-application-with-logos-sql.md) leaves retry decisions to the user.\n\nThe flow is: catch the blocked-write error, list the displaced writes, inspect the current data, then retry or continue without each write. The [password-manager guide](https://docs.logos.co/blockchain/logos-sql/build-application-with-logos-sql.md) shows the terminal commands for this. This page explains the library calls behind them.\n\n> **Info: Prerequisites**\n>\n>\n> - A running `LogosSql` writer and access to its current database.\n> - An application write path that calls `execute` and handles its result.\n>\n\n## What to expect\n\n- You can identify local writes that need an application decision.\n- You can retry a selected write or mark it handled without retrying it.\n- You can keep the review correct when more displacements arrive in the meantime.\n\n## Step 1: Recognise a blocked write\n\n1. When submitting a transaction, handle `Error::UnhandledDisplacements` separately from other errors:\n\n   ```rust\n   use logos_sql::Error;\n\n   match db.execute(transaction).await {\n       Ok(tx_id) => println!(\"Committed locally as {tx_id}\"),\n       Err(Error::UnhandledDisplacements) => {\n           println!(\"Earlier writes were displaced. Review them before continuing.\");\n       }\n       Err(error) => return Err(error.into()),\n   }\n   ```\n\n   This error means earlier writes need review. The new transaction has not run, and no changes from it were committed.\n\n1. Pause ordinary submissions and show the review flow below. Reads still work, so the application can inspect what is in the database now.\n\n## Step 2: Find out what was displaced\n\n1. Fetch the writes awaiting review:\n\n   ```rust\n   let displacements = db.unhandled_displacements().await?;\n\n   for displacement in &displacements {\n       println!(\"{}: {:?}\", displacement.tx_id, displacement.reason);\n   }\n   ```\n\n   Each entry identifies one of this participant's writes by its transaction ID and gives the reason it was displaced.\n\n   With multiple writers, another participant's writes can arrive while yours are still waiting to be published. A chain reorganisation can also remove accepted writes, even with only one writer. The reasons below distinguish writes removed from [channel](https://docs.logos.co/get-started/glossary.md#channel) history from those removed while still waiting locally:\n\n   | Reason | Meaning |\n   | --- | --- |\n   | `Orphaned` | The [Zone SDK](https://docs.logos.co/get-started/glossary.md#zone-sdk) removed a previously tracked write from channel history, for example because a competing channel write replaced it. This can happen without a blockchain reorganisation. |\n   | `PendingWriteInvalidated` | The write was still waiting locally to be published when other channel writes or a chain reorganisation changed the database it was based on. Logos SQL removed the local write rather than automatically applying it again to the changed data. |\n\n   Neither reason means the SQL failed a constraint. Even writes to unrelated rows can compete for a channel position.\n\n1. Use the transaction ID to identify the original operation, then read the current data. In the password manager example, the user can use `show` or `list` before deciding what to do.\n\n   > **Tip**\n   >\n   > If your application needs descriptions such as “changed the email password”, save that description with the transaction ID when preparing the original write. The builder exposes it through `transaction.tx_id()`. Save the association persistently if it must survive restart. Logos SQL already retains the SQL needed for a retry; the application does not need to store another copy.\n\n1. Keep the returned `Displacement` value for the retry or handle call. It identifies the specific item you reviewed, so an old action cannot clear a newer displacement of the same write.\n\n   A write that returns to live history or finalises no longer appears in the review list. Refresh the list before showing old review items after a long pause or restart.\n\n## Step 3: Retry or continue without the write\n\nChoose separately for each displaced write. The password manager asks the user; another application may have a policy for particular operations.\n\n1. Compare the intended operation with the current data. Choose whether to retry its original SQL, leave it without a retry, or prepare a different change.\n\n   For example, the password manager checks that the stored password still matches the encrypted value the application originally read:\n\n   ```rust\n   fn prepare_password_update(\n       label: &str,\n       password: &[u8],\n       old_password: &[u8],\n   ) -> TransactionBuilder {\n       TransactionBuilder::new(\n           \"UPDATE credentials SET password = ?2\n            WHERE label = ?1 AND password = ?3\",\n       )\n       .bind(label)\n       .bind(password)\n       .bind(old_password)\n   }\n   ```\n\n   - Both parameters contain encrypted bytes. The application encrypts the replacement before preparing the SQL and keeps the original stored bytes for the condition. A retry reuses both values without encrypting them again.\n   - The condition and update execute together. Changes to notes or other entries do not affect this condition. If this entry's stored password is different, the statement changes no rows.\n   - SQLite considers zero matching rows successful. `execute` returns a transaction ID, not an affected-row count, so success alone does not tell the application whether this update changed the password.\n\n1. Decide which operations the application can retry automatically. Their SQL conditions should express the assumptions that must still hold, and the application must decide what to do when those conditions fail.\n\n   The password-manager example uses manual review as its policy. Logos SQL does not require a person to make this decision; applications can use the same retry and handle calls automatically where appropriate.\n\nOnce you have decided, use one of the following calls for the selected `displacement`.\n\n### Option A—Retry the original SQL\n\n1. Pass the selected entry to the retry method:\n\n   ```rust\n   use logos_sql::Error;\n\n   match db.retry_displacement(&displacement).await {\n       Ok(new_tx_id) => println!(\"Retry committed locally as {new_tx_id}\"),\n       Err(Error::StaleDisplacement) => {\n           // A reorganisation may have made the branch containing this write\n           // canonical again. Fetch the updated list before retrying.\n       }\n       Err(error) => return Err(error.into()),\n   }\n   ```\n\n   A successful call executes the original SQL and parameters, creates a new transaction ID and marks the selected displacement handled. This is allowed while other displacements await review. Time and random functions are evaluated again for the new write.\n\n   If the call fails or is interrupted, the retry may already have committed. Do not respond to every error by submitting the same operation again.\n\n### Option B—Continue without retrying the original SQL\n\n1. Mark the selected entry handled:\n\n   ```rust\n   db.mark_displacement_handled(displacement).await?;\n   ```\n\n   This records that the application has considered it. No replacement write is submitted. Once the remaining displacements are handled, the application may submit a different transaction based on the current data.\n\n> **Warning: Marking handled does not withdraw the write**\n>\n> Marking a displacement handled clears the application's review item; it does not erase the write from the branch that contained it. If that branch becomes canonical again, Logos SQL rebuilds the database from its history and the write returns, even if the application chose not to retry it.\n\n## Step 4: Continue writing\n\n1. Refresh `unhandled_displacements()` after handling the reviewed items. A newly arrived displacement still needs a decision even if you finished an earlier batch.\n\n1. Re-read the data needed for subsequent writes and prepare them again where necessary. Coordinate this with other application tasks that submit writes.\n\n   The write block belongs to the whole participant. Once the list is clear, it does not protect another task from submitting work prepared before the displacement.\n\n1. Submit the next operation and continue to handle `Error::UnhandledDisplacements`. A new displacement can arrive between the list query and the write; the execution check will block that write if it does.\n\n## Troubleshooting\n\n### A reviewed item returns StaleDisplacement\n\nThe selected occurrence was handled, cleared or replaced before the retry. Refresh the list. Do not retry it through ordinary `execute` to bypass this check.\n\n### Writes remain blocked after review\n\nCheck the current review list. Another displacement may have arrived while the application was reviewing earlier entries. Handling an earlier entry does not clear the new one.\n\n### A handled write becomes live again\n\nThis can happen when the chain returns to a branch containing the original write. Use `write_status(tx_id)` and current database reads to update the application's view. Handling a displacement records a local decision, not a permanent chain outcome.\n"},{"id":"blockchain/faq-logos-blockchain","title":"Blockchain FAQ","summary":"Review answers to less obvious questions about how the Logos Blockchain works and where it fits.","section":"blockchain","doc_type":"concept","url":"https://docs.logos.co/blockchain/faq","markdown_url":"https://docs.logos.co/blockchain/faq.md","permalink":"/blockchain/faq","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/faq-logos-blockchain.md","headings":[{"depth":2,"text":"How is the Logos Blockchain private if Mantle notes are public UTXOs?"},{"depth":2,"text":"Does the Blend Network always protect proposer privacy for everyone?"},{"depth":2,"text":"If Bedrock doesn't interpret Zone data, what secures funds bridged into a Zone?"},{"depth":2,"text":"Does decentralised sequencing eliminate MEV on a Zone?"},{"depth":2,"text":"Why build on a Zone instead of launching an independent Layer 1?"}],"markdown":"# Blockchain FAQ\n\n#### Review answers to less obvious questions about how the Logos Blockchain works and where it fits.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n## How is the Logos Blockchain private if Mantle notes are public UTXOs?\n\nIt is important to distinguish between two distinct kinds of privacy that are possible on the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain): transaction privacy, and proposer privacy. Transaction privacy obscures transactions and other information published on-chain, preventing third parties from learning what exactly an individual did when interacting with the blockchain. On the other hand, proposer privacy makes it difficult to link a proposed block to whoever proposed it: it is computationally infeasible for an observer to determine which participant is behind a given block, either before or after it's added to the chain. In other words, **transaction privacy hides the data published to the chain, while proposer privacy protects the people running the infrastructure**. The latter is particularly relevant for Proof of Stake blockchains, which select block proposers according to a schedule generated ahead of time.\n\nBecause the Logos Blockchain's Bedrock layer (which maintains the [Mantle ledger](https://docs.logos.co/blockchain/concepts/about-mantle.md#mantle-ledger)) serves a resilient foundational layer for apps running on Logos Zones, it is designed primarily to protect the privacy of block proposers. If proposers could be identified or connected with transactions included in their blocks, they could become targets for coercion or denial of service attacks by hostile actors. Even when there is no credible threat of being targeted, block proposers without privacy could engage in cautionary self-censorship by excluding certain transactions, thereby compromising the neutrality of the entire network. Protecting proposer privacy helps prevent this regardless of how transparent the ledger is.\n\nTransaction privacy needs differ vastly for different use cases, so this type of privacy is not provided directly by Bedrock for most cases. [Notes](https://docs.logos.co/get-started/glossary.md#note) and their transfers are recorded openly on the ledger, similar to Bitcoin, and the Bedrock layer makes no attempt to hide that data. Despite this, it is possible to mine new notes and write arbitrary data on-chain in a private way by submitting these types of transactions via the [Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network). Bedrock therefore supports privacy-preserving consensus participation and [inscriptions](https://docs.logos.co/get-started/glossary.md#inscription), but not note transfers or other transaction types.\n\nDevelopers that need to protect their app's data, such as balances or message contents, must implement their desired level of transaction privacy at the [Zone](https://docs.logos.co/blockchain/concepts/about-zones.md) layer instead of relying on Bedrock for it. **Transaction privacy is supported natively on the [LEZ](https://docs.logos.co/get-started/glossary.md#lez), in addition to public transactions**.\n\n> **Note**\n>\n> For more information on proposer privacy on Logos, see [Anonymous Block Proposers](https://blog.logos.co/article/anonymous-block-proposers).\n\n## Does the Blend Network always protect proposer privacy for everyone?\n\nNo, that protection depends on whether the proposer is a [core node](https://docs.logos.co/get-started/glossary.md#core-node), and how many other core nodes are active in a given [epoch](https://docs.logos.co/get-started/glossary.md#epoch). Only declared core nodes relay and blend messages, and their participation in Blend activity makes it hard to distinguish when they publish their own proposals. A block proposer that hasn't declared as a core node participates as an edge node instead, sending its proposal through core nodes for blending - an unusual activity that can be singled out and connected to published blocks by some attackers. If too few core nodes are active, all proposers fall back to broadcasting directly, with no Blend privacy for that epoch.\n\n## If Bedrock doesn't interpret Zone data, what secures funds bridged into a Zone?\n\nCorrectness is the Zone's own responsibility, not Bedrock's. [Bedrock](https://docs.logos.co/blockchain/concepts/about-bedrock.md) stores whatever state updates a [Zone](https://docs.logos.co/blockchain/concepts/about-zones.md)'s sequencer inscribes without verifying their validity, so each Zone chooses its own correctness mechanism - publishing zero knowledge validity proofs, running a fraud-proof challenge window, or requiring its nodes to re-execute the state transition themselves. For bridged funds specifically, Bedrock enforces only one aggregate invariant: the total note value withdrawn from a Zone can never exceed the total value deposited into it. It doesn't verify that a withdrawal went to the correct account or that a Zone's internal accounting is honest - that guarantee comes entirely from whichever correctness mechanism the Zone itself implements.\n\nOn the LEZ, transactions are verified differently depending on if they're public or private. Public transactions are re-executed by LEZ validators, while private transactions involve the generation of zero knowledge proofs via Risc0, which are then verified by LEZ validators.\n\n## Does decentralised sequencing eliminate MEV on a Zone?\n\nNo, it distributes the opportunity for MEV extraction across a set of sequencers rather than removing it. When a Zone uses several sequencers, taking turns on a round-robin schedule or competing to publish first, no single sequencer can reliably capture all of the MEV available the way a single-sequencer rollup's operator could. A threshold of sequencers can also jointly sign a change to the sequencer list, letting an honest majority remove a malicious one.\n\nBy isolating MEV to the Zone level, the Logos Blockchain is able to remain neutral and decentralised - centralised sequencing in one Zone does not impact the decentalisation of other Zones.\n\n## Why build on a Zone instead of launching an independent Layer 1?\n\nLaunching a new Layer 1 means bootstrapping its own validator set, economic security, and data availability from scratch, which a young chain typically struggles to do. Building as a [Zone](https://docs.logos.co/blockchain/concepts/about-zones.md) instead lets an application keep full control over its own state and execution environment while relying on the Logos Blockchain for consensus guarantees and data availability it couldn't easily provide on its own. A Zone isn't locked into using Bedrock's other features to get this: it can run as a fully independent sovereign rollup, using Bedrock only for ordering and data availability, or opt into decentralised sequencing and token bridging for extra interoperability without giving up the sovereign rollup model's benefits.\n"},{"id":"blockchain/reference/blockchain-api-reference","title":"Logos Blockchain API Reference","summary":"This page describes every HTTP endpoint a Logos Blockchain node exposes. It's accurate for Logos Blockchain node 0.3.1.","section":"blockchain","doc_type":"reference","url":"https://docs.logos.co/blockchain/reference/blockchain-api-reference","markdown_url":"https://docs.logos.co/blockchain/reference/blockchain-api-reference.md","permalink":"/blockchain/reference/blockchain-api-reference","source":"https://github.com/logos-co/logos-docs/blob/main/docs/blockchain/reference/blockchain-api-reference.md","headings":[{"depth":2,"text":"Conventions"},{"depth":3,"text":"Access and security"},{"depth":3,"text":"Error responses"},{"depth":3,"text":"Requests while the node is bootstrapping"},{"depth":2,"text":"Node"},{"depth":3,"text":"`GET /version`"},{"depth":3,"text":"`GET /chain/id`"},{"depth":2,"text":"Consensus"},{"depth":3,"text":"`GET /cryptarchia/info`"},{"depth":3,"text":"`GET /time/info`"},{"depth":3,"text":"`GET /cryptarchia/headers`"},{"depth":3,"text":"`GET /cryptarchia/blocks/{id}`"},{"depth":3,"text":"`GET /cryptarchia/blocks/{id}/events`"},{"depth":3,"text":"`GET /cryptarchia/blocks`"},{"depth":3,"text":"`GET /cryptarchia/blocks_range`"},{"depth":3,"text":"`GET /cryptarchia/events/blocks/stream`"},{"depth":3,"text":"`GET /cryptarchia/lib-stream`"},{"depth":3,"text":"`GET /cryptarchia/transaction/{id}`"},{"depth":2,"text":"Mantle and mempool"},{"depth":3,"text":"`GET /mantle/metrics`"},{"depth":3,"text":"`GET /mempool/view`"},{"depth":3,"text":"`POST /mantle/status`"},{"depth":3,"text":"`POST /mempool/add/tx`"},{"depth":3,"text":"`GET /mantle/gas-prices`"},{"depth":3,"text":"`GET /channel/{id}`"},{"depth":3,"text":"`POST /channel/deposit`"},{"depth":2,"text":"Service Declaration Protocol"},{"depth":3,"text":"`GET /mantle/sdp/declarations`"},{"depth":3,"text":"`GET /mantle/sdp/snapshot`"},{"depth":3,"text":"`POST /sdp/declaration`"},{"depth":3,"text":"`POST /sdp/activity`"},{"depth":3,"text":"`POST /sdp/withdrawal`"},{"depth":3,"text":"`POST /sdp/set-declaration-id`"},{"depth":2,"text":"Network and Blend"},{"depth":3,"text":"`GET /network/info`"},{"depth":3,"text":"`POST /network/dial_peer`"},{"depth":3,"text":"`GET /blend/info`"},{"depth":3,"text":"`POST /blend/join`"},{"depth":3,"text":"`POST /blend/transactions/disperse`"},{"depth":3,"text":"`GET /blend/transactions/pending`"},{"depth":2,"text":"Leader rewards"},{"depth":3,"text":"`GET /leader/aged-notes`"},{"depth":3,"text":"`GET /leader/claim/vouchers`"},{"depth":3,"text":"`POST /leader/claim`"},{"depth":2,"text":"Wallet"},{"depth":3,"text":"`GET /wallet/{public_key}/balance`"},{"depth":3,"text":"`POST /wallet/transactions/transfer-funds`"},{"depth":3,"text":"`POST /wallet/fund`"},{"depth":3,"text":"`POST /wallet/sign/ed25519`"},{"depth":3,"text":"`POST /wallet/sign/zk`"},{"depth":2,"text":"Proof-of-Work"},{"depth":3,"text":"`GET /pow/status`"},{"depth":3,"text":"`GET /pow/rewards/claimable`"},{"depth":3,"text":"`PUT /pow/mining/start`"},{"depth":3,"text":"`PUT /pow/mining/stop`"},{"depth":3,"text":"`PUT /pow/auto-claim/start`"},{"depth":3,"text":"`PUT /pow/auto-claim/stop`"},{"depth":3,"text":"`POST /pow/claim`"},{"depth":2,"text":"Administration"},{"depth":3,"text":"`PUT /admin/tracing/filter`"}],"markdown":"# Logos Blockchain API Reference\n\nThis page describes every HTTP endpoint a [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) node exposes. It's accurate for Logos Blockchain node `0.3.1`.\n\n> **Tip**\n>\n> If you have access to a running Blockchain node, you can view the OpenAPI specification at `/api-docs/openapi.json` and a Swagger UI at `/swagger-ui/` on the same port.\n\n## Conventions\n\n- The API listens on `127.0.0.1:8080` by default. To change the address, set `api.backend.listen_address` in the node's configuration.\n- Requests and responses use JSON unless an endpoint says otherwise. Send request bodies with the `Content-Type: application/json` header.\n- A few successful responses have a JSON body but a `text/plain` content type: [`GET /leader/aged-notes`](#get-leaderaged-notes), [`GET /leader/claim/vouchers`](#get-leaderclaimvouchers), [`GET /wallet/{public_key}/balance`](#get-walletpublic_keybalance), and [`POST /wallet/transactions/transfer-funds`](#post-wallettransactionstransfer-funds). Parse the body as JSON regardless of the content type.\n- Hashes, block IDs, transaction IDs, [note](https://docs.logos.co/get-started/glossary.md#note) IDs, declaration IDs, and ZK public keys are 32-byte values encoded as 64-character hexadecimal strings without a `0x` prefix.\n- Peer IDs are libp2p peer IDs encoded as base58 strings. Network addresses and [locators](https://docs.logos.co/get-started/glossary.md#locator) are libp2p `multiaddr` strings, such as `/ip4/203.0.113.10/udp/3000/quic-v1`.\n- [Slots](https://docs.logos.co/get-started/glossary.md#slot), [epochs](https://docs.logos.co/get-started/glossary.md#epoch), and token amounts are unsigned integers.\n- Path parameters are shown in braces. For example, in `GET /cryptarchia/blocks/{id}`, replace `{id}` with a block ID.\n\n### Access and security\n\nThe API has no authentication. Anyone who can reach it can read the node's wallet balances, sign arbitrary transaction hashes with the node's keys, and spend its funds. Keep the API bound to `127.0.0.1`, or put it behind an authenticating proxy, and don't expose it to the public internet.\n\n### Error responses\n\nWhen the node handles a request and it fails, the response has a non-`200` status code and the following body:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `code` | integer | The HTTP status code of the response. |\n| `message` | string | A human-readable description of the error. |\n\nMost failures return `500`. Endpoints that look up a single item return `404` when the item doesn't exist, and [`GET /cryptarchia/blocks_range`](#get-cryptarchiablocks_range) returns `400` for invalid query parameters.\n\nSome errors don't use this body:\n\n- A malformed path parameter, query string, or request body returns `400`, `415`, or `422` with a plain-text message, such as `Failed to deserialize query string: missing field 'slot_from'`.\n- An unknown path returns `404` with an empty body.\n- A request that takes longer than the API timeout returns `408` with an empty body. The timeout is 30 seconds by default. To change it, set `api.backend.timeout`, in seconds, in the node's configuration.\n\n### Requests while the node is bootstrapping\n\nWhile the node is in the `Bootstrapping` state, the following endpoints don't respond until the node is `Online`. Each request waits until the API timeout and then returns `408`:\n\n- [`GET /blend/info`](#get-blendinfo)\n- [`GET /blend/transactions/pending`](#get-blendtransactionspending)\n- [`GET /pow/status`](#get-powstatus)\n- [`GET /pow/rewards/claimable`](#get-powrewardsclaimable)\n- [`POST /pow/claim`](#post-powclaim)\n\nThe other read endpoints on this page respond normally while the node is [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping).\n\n## Node\n\n### `GET /version`\n\nGets the version and build information of the running node.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `version` | string | The version of the node binary, such as `0.3.1`. |\n| `commit` | string or `null` | The abbreviated commit the binary was built from. `null` if it wasn't built from a Git checkout. |\n| `tag` | string or `null` | The Git tag that points at that commit. `null` if there is none. |\n| `target` | string | The target triple the binary was compiled for, such as `x86_64-unknown-linux-gnu`. |\n| `profile` | string | The Cargo profile the binary was compiled with, such as `release`. |\n| `rustc` | string | The version of the Rust compiler that built the binary. |\n\n### `GET /chain/id`\n\nGets the ID of the chain the node runs on. The chain ID is fixed by the node's deployment and doesn't change while the node runs.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `chain_id` | string | The chain ID. |\n\n## Consensus\n\n### `GET /cryptarchia/info`\n\nGets the consensus state information for the node.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `cryptarchia_info` | object | The node's current view of the chain. |\n| `cryptarchia_info.lib` | string | The ID of the latest immutable block (LIB), the most recent block that can no longer be reorganised. |\n| `cryptarchia_info.lib_slot` | integer | The slot of the latest immutable block. |\n| `cryptarchia_info.tip` | string | The ID of the block at the tip of the node's canonical chain. |\n| `cryptarchia_info.slot` | integer | The slot of the tip block. |\n| `cryptarchia_info.height` | integer | The height of the tip block, which is the length of the canonical chain. |\n| `cryptarchia_info.state` | string | The state of the consensus engine: `Bootstrapping` or `Online`. |\n| `phase` | string | The phase the chain service is in. See the following table. |\n\n`phase` has one of the following values:\n\n| Value | Description |\n| --- | --- |\n| `AwaitingGenesisTime` | The genesis time is in the future. The node serves only read-only queries. |\n| `InitialBlockDownload` | The node applies blocks while it waits for the Initial Block Download to complete. |\n| `ProlongedBootstrapPeriod` | The Prolonged Bootstrap Period is running. |\n| `Following` | The node follows the chain in real time. |\n\n### `GET /time/info`\n\nGets the node's slot and epoch timing information.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `slot_duration_ms` | integer | The duration of a slot, in milliseconds. |\n| `genesis_time_unix_ms` | integer | The genesis time of the chain, in milliseconds since the Unix epoch. |\n| `current_slot` | integer | The current slot. |\n| `current_epoch` | integer | The current epoch. |\n| `slots_per_epoch` | integer | The number of slots in an epoch. |\n\n### `GET /cryptarchia/headers`\n\nGets the IDs of the blocks on a branch of the chain, walking from a descendant block back towards an ancestor.\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `from` | string | No | The ID of the block to start from. Defaults to the tip of the node's canonical chain. |\n| `to` | string | No | The ID of the ancestor block to stop at. Defaults to the latest immutable block. |\n\n#### Response\n\nAn array of block IDs, newest first, including both `from` and `to`. The response contains at most 512 IDs.\n\n### `GET /cryptarchia/blocks/{id}`\n\nGets a block by its ID.\n\n#### Path parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `id` | string | The ID of the block. |\n\n#### Response fields\n\nReturns `404` if the node doesn't store the block.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `header` | object | The block header. |\n| `header.id` | string | The ID of the block. |\n| `header.parent_block` | string | The ID of the parent block. |\n| `header.slot` | integer | The slot the block was proposed in. |\n| `header.body_root` | string | The root of the block body's Merkle tree. |\n| `header.proof_of_leadership` | object | The proposer's [Proof of Leadership](https://docs.logos.co/get-started/glossary.md#proof-of-leadership). It contains `proof`, `entropy_contribution`, `leader_key`, and `voucher_cm`. |\n| `uncle_headers` | array of objects | The signed headers of uncle blocks referenced by this block. Each entry has a `header` object in the same format as `header`, and a `signature` string. |\n| `transactions` | array of objects | The transactions in the block, in the format that [`GET /cryptarchia/transaction/{id}`](#get-cryptarchiatransactionid) returns. |\n\n### `GET /cryptarchia/blocks/{id}/events`\n\nGets the ledger events produced when processing a block, including reward claims and channel deposits.\n\n#### Path parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `id` | string | The ID of the block. |\n\n#### Response\n\nAn array of events. Each event is an object with a single key that names the event kind, such as `Tx` for an event produced by a transaction. A `Tx` event contains the `tx_hash` of the transaction, the `op_id` of the operation, and a `payload` object whose single key names the payload type, such as `PoWRewardClaimed`, `LeaderRewardClaimed`, or `Deposit`.\n\nReturns `404` if the node has no events for the block.\n\n### `GET /cryptarchia/blocks`\n\nGets the immutable blocks in a slot range.\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `slot_from` | integer | Yes | The first slot of the range. |\n| `slot_to` | integer | Yes | The last slot of the range, inclusive. |\n\n#### Response\n\nAn array of blocks in the format that [`GET /cryptarchia/blocks/{id}`](#get-cryptarchiablocksid) returns. The array is empty if the range contains no immutable blocks. To include blocks that aren't immutable yet, use [`GET /cryptarchia/blocks_range`](#get-cryptarchiablocks_range).\n\n### `GET /cryptarchia/blocks_range`\n\nStreams the blocks in a slot range, with the chain state at the time each block was processed.\n\n#### Query parameters\n\nAll query parameters are optional.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `slot_from` | integer | The first slot of the range. For a descending stream, defaults to `0`. For an ascending stream, the node estimates a lower bound that makes the stream end near `slot_to`, so the stream can return fewer than `blocks_limit` blocks. |\n| `slot_to` | integer | The last slot of the range, inclusive. Defaults to the slot of the tip, or of the latest immutable block if `block_filter` is `immutable_only`. A value past that slot is lowered to it. |\n| `order` | string | `descending` (newest first) or `ascending` (oldest first). Defaults to `descending`. |\n| `blocks_limit` | integer | The maximum number of blocks to return, from `1` to `630720000`. Defaults to `630720000` if you set both `slot_from` and `slot_to`, and to `100` otherwise. |\n| `server_batch_size` | integer | How many blocks the node fetches at a time, from `1` to `1000`. Defaults to `100`. |\n| `block_filter` | string | `immutable_only` to return only immutable blocks, or `mutable_and_immutable` to return all blocks on the canonical chain. Defaults to `mutable_and_immutable`. |\n\n#### Response\n\nA stream of newline-delimited JSON objects, with the content type `application/x-ndjson`. Each line has the following fields:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `block` | object | The block, in the format that [`GET /cryptarchia/blocks/{id}`](#get-cryptarchiablocksid) returns. |\n| `tip` | string | The ID of the tip when the block was processed. |\n| `tip_slot` | integer | The slot of that tip. |\n| `lib` | string | The ID of the latest immutable block when the block was processed. |\n| `lib_slot` | integer | The slot of that latest immutable block. |\n\nReturns `400` if `slot_from` is greater than `slot_to` or a parameter is out of range. If the node hits an error after the stream has started, the stream ends early without reporting the error.\n\n### `GET /cryptarchia/events/blocks/stream`\n\nStreams each new block the node processes, from the time of the request onwards. The response is a stream of newline-delimited JSON objects in the same format as [`GET /cryptarchia/blocks_range`](#get-cryptarchiablocks_range). The stream stays open until the client closes it.\n\n### `GET /cryptarchia/lib-stream`\n\nStreams each block that becomes immutable, from the time of the request onwards. The response is a stream of newline-delimited JSON objects, with the content type `application/x-ndjson`. The stream stays open until the client closes it. Each line has the following fields:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `height` | integer | The height of the block. |\n| `header_id` | string | The ID of the block. |\n\n### `GET /cryptarchia/transaction/{id}`\n\nGets a transaction that's included in a block the node stores.\n\n#### Path parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `id` | string | The hash of the transaction. |\n\n#### Response fields\n\nReturns `404` if the node doesn't store the transaction.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `mantle_tx` | object | The unsigned transaction. |\n| `mantle_tx.hash` | string | The hash of the transaction. |\n| `mantle_tx.ops` | array of objects | The operations in the transaction. Each has a numeric `opcode` and a `payload` object whose fields depend on the operation. |\n| `ops_proofs` | array of objects | The proofs that authorise each operation, in the same order as `mantle_tx.ops`. Each entry is an object with a single key that names the proof type, such as `ZkSig`. An operation that needs no proof has the entry `{\"None\": null}`. |\n\n## Mantle and mempool\n\n### `GET /mantle/metrics`\n\nGets metrics about the node's transaction mempool.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `pending_items` | integer | The number of transactions pending in the mempool. |\n| `last_item_timestamp` | integer | The time the most recent transaction was added to the mempool, in milliseconds since the Unix epoch. `0` if no transaction has been added. |\n\n### `GET /mempool/view`\n\nGets the hashes of the transactions in the mempool that can be included on top of the current tip.\n\n#### Response\n\nAn array of transaction hashes.\n\n### `POST /mantle/status`\n\nGets the mempool status of one or more transactions.\n\n#### Request body\n\nAn array of transaction hashes.\n\n#### Response\n\nAn array with one status per requested hash, in request order. Each status is `Pending` if the transaction is in the mempool, or `Unknown` if it isn't.\n\n### `POST /mempool/add/tx`\n\nAdds a signed transaction to the node's mempool, which then shares it with the network.\n\n#### Request body\n\nA signed transaction, as an object with `mantle_tx` and `ops_proofs` fields in the format that [`GET /cryptarchia/transaction/{id}`](#get-cryptarchiatransactionid) returns.\n\n#### Response\n\nReturns `200` with a `null` body when the node accepts the transaction.\n\n### `GET /mantle/gas-prices`\n\nGets the gas prices from the ledger state at a block.\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tip` | string | No | The ID of the block to read the ledger state at. Defaults to the current tip. |\n\n#### Response fields\n\nReturns `404` if the node has no ledger state for the block.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tip` | string | The ID of the block the prices were read at. |\n| `execution_base_gas_price` | integer | The base price per unit of execution gas. |\n| `storage_gas_price` | integer | The price per unit of storage gas. |\n\n### `GET /channel/{id}`\n\nGets the current state of a [channel](https://docs.logos.co/get-started/glossary.md#channel).\n\n#### Path parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `id` | string | The ID of the channel. |\n\n#### Response fields\n\nReturns `404` if the channel doesn't exist.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `accredited_keys` | array | The keys that are authorised to post to the channel. |\n| `configuration_threshold` | integer | The number of keys required to change the channel's configuration. |\n| `tip_message` | string | The ID of the channel's latest [message](https://docs.logos.co/get-started/glossary.md#message). |\n| `config_tip_hash` | string | The ID of the channel's latest configuration message. |\n| `tip_slot` | integer | The slot of the channel's latest message. |\n| `tip_sequencer` | integer | The position, in `accredited_keys`, of the key whose turn it is to post. |\n| `tip_sequencer_starting_slot` | integer | The slot at which the current sequencer's turn started. |\n| `posting_timeframe` | integer | The number of slots in each sequencer's turn. `0` means unlimited. |\n| `posting_timeout` | integer | The number of slots a sequencer can stay silent before the turn passes to the next key. `0` means no timeout. |\n| `transfer_threshold` | integer | The number of keys required to transfer funds out of the channel. |\n\n### `POST /channel/deposit`\n\nBuilds a channel deposit transaction, funds and signs it with the node's wallet, and adds it to the mempool.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `deposit` | object | Yes | The deposit operation. It contains the `channel_id` to deposit into, the `inputs` (an array of note IDs to deposit), and `metadata`. |\n| `change_public_key` | string | Yes | The ZK public key that receives any change. |\n| `funding_public_keys` | array of strings | Yes | The ZK public keys whose notes the wallet can spend to pay the fee. |\n| `max_tx_fee` | integer | Yes | The maximum fee the transaction can pay. The request fails if the fee is higher. |\n| `tip` | string | No | Accepted, but ignored. The node always funds the deposit against the current tip. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `hash` | string | The hash of the submitted transaction. |\n\n## Service Declaration Protocol\n\nThese endpoints read and submit [Service Declaration Protocol](https://docs.logos.co/get-started/glossary.md#service-declaration-protocol) (SDP) declarations. In `0.3.1`, the only service type is the [Blend Network](https://docs.logos.co/get-started/glossary.md#blend-network), shown as `BN`.\n\n### `GET /mantle/sdp/declarations`\n\nGets every declaration in the current SDP registry.\n\n#### Response fields\n\nAn object that maps each declaration ID to a declaration with the following fields:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `service_type` | string | The service the declaration is for. Always `BN` in `0.3.1`. |\n| `provider_id` | string | The provider's public key. |\n| `service_note_id` | string | The ID of the note locked for the declaration. |\n| `locators` | array of strings | The `multiaddr` addresses where the provider can be reached. |\n| `zk_id` | string | The ZK public key of the provider. |\n| `created` | integer | The epoch of the block that contained the declaration. |\n| `active` | integer | The latest epoch for which the provider sent an activity message. |\n| `withdraw_at` | integer or `null` | The epoch at which the declaration is scheduled to be withdrawn. `null` if no withdrawal is scheduled. |\n| `nonce` | integer | The declaration's nonce, which increases with each update. |\n\n### `GET /mantle/sdp/snapshot`\n\nGets the SDP declarations in the snapshot for the current epoch, which is the set the network uses during that epoch. The response has the same format as [`GET /mantle/sdp/declarations`](#get-mantlesdpdeclarations).\n\n### `POST /sdp/declaration`\n\nBuilds a declaration transaction, funds and signs it with the node's wallet, and adds it to the mempool.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `service_type` | string | Yes | The service to declare. Must be `BN`. |\n| `locators` | array of strings | Yes | The `multiaddr` addresses where the node can be reached. At least one is required. |\n| `provider_id` | string | Yes | The provider's public key. |\n| `zk_id` | string | Yes | The ZK public key of the provider. |\n| `service_note_id` | string | Yes | The ID of the note to lock for the declaration. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `declaration_id` | string | The ID of the declaration. |\n| `tx_id` | string | The hash of the submitted transaction. |\n\n### `POST /sdp/activity`\n\nSubmits an activity message that proves the node's service was active during an epoch.\n\n#### Request body\n\nThe activity metadata. In `0.3.1`, this is an object with a single `Blend` key that holds a Blend activity proof.\n\n#### Response fields\n\nThe response has the same format as [`POST /sdp/declaration`](#post-sdpdeclaration). The request fails if the node hasn't set a current declaration with [`POST /sdp/set-declaration-id`](#post-sdpset-declaration-id).\n\n### `POST /sdp/withdrawal`\n\nWithdraws an SDP declaration.\n\n#### Request body\n\nThe ID of the declaration to withdraw, as a JSON string.\n\n#### Response fields\n\nThe response has the same format as [`POST /sdp/declaration`](#post-sdpdeclaration). After a successful withdrawal, the node no longer has a current declaration.\n\n### `POST /sdp/set-declaration-id`\n\nSets the declaration the node uses for its own SDP messages, such as activity messages.\n\n#### Request body\n\nThe declaration ID as a JSON string, or `null` to clear the current declaration. The request fails if the declaration doesn't exist.\n\n#### Response\n\nReturns `200` with a `null` body on success.\n\n## Network and Blend\n\n### `GET /network/info`\n\nGets information about the node's peer connectivity.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `listen_addresses` | array of strings | The addresses the node listens on. |\n| `peer_id` | string | The peer ID of this node. |\n| `connected_peers` | array of strings | The peer IDs of the peers the node is connected to. |\n| `discovered_peers` | array of strings | The peer IDs of the peers the node has discovered through Kademlia. A discovered peer isn't necessarily connected. |\n| `n_peers` | integer | The number of connected peers. |\n| `n_connections` | integer | The total number of connections, both pending and established. A peer can have more than one connection. |\n| `n_discovered_peers` | integer | The number of entries in `discovered_peers`. |\n| `n_pending_connections` | integer | The number of connections, incoming and outgoing, that aren't established yet. |\n\n### `POST /network/dial_peer`\n\nConnects the node to a peer.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `addr` | string | Yes | The `multiaddr` address of the peer. |\n\n#### Response\n\nThe peer ID of the connected peer, as a JSON string.\n\n### `GET /blend/info`\n\nGets information about the node's view of the Blend network.\n\n#### Response fields\n\nThe response is `null` if the Blend service has no network information to report.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `node_id` | string | The peer ID of this node in the Blend network. |\n| `core_info` | object or `null` | Peer information for a node that participates in the Blend network as a [core node](https://docs.logos.co/get-started/glossary.md#core-node). `null` if the node runs as an edge or broadcast node. |\n| `core_info.current_epoch_peers` | array | The peers negotiated for the current epoch. Each entry is a two-element array: the peer ID (string) and whether the peer is healthy (boolean). |\n| `core_info.old_epoch_peers` | array of strings or `null` | The peer IDs negotiated for the previous epoch while an epoch transition is in progress. `null` if no transition is in progress. |\n\n### `POST /blend/join`\n\nDeclares the node as a Blend Network core node by submitting an SDP declaration.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `locator` | string | Yes | The `multiaddr` address where other Blend nodes can reach this node, such as `/ip4/203.0.113.10/udp/3000/quic-v1`. |\n| `service_note_id` | string | Yes | The ID of the note to lock for the declaration. |\n\n#### Response fields\n\nThe response has the same format as [`POST /sdp/declaration`](#post-sdpdeclaration).\n\n### `POST /blend/transactions/disperse`\n\nSends a signed transaction through the Blend Network instead of adding it to this node's mempool.\n\n#### Request body\n\nA signed transaction, in the same format as [`POST /mempool/add/tx`](#post-mempooladdtx).\n\n#### Response\n\nThe hash of the transaction, as a JSON string.\n\n### `GET /blend/transactions/pending`\n\nGets the hashes of the transactions waiting for a PoW solution before the node can blend them.\n\n#### Response\n\nAn array of transaction hashes.\n\n## Leader rewards\n\n### `GET /leader/aged-notes`\n\nGets the wallet's notes that are eligible to take part in the leadership lottery. A note is eligible when it's in the aged note snapshot for the current epoch and the wallet holds its key.\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tip` | string | No | The ID of the block to check eligibility at. Defaults to the current tip. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tip` | string | The ID of the block eligibility was checked at. |\n| `notes` | array of objects | The eligible notes. Empty if the node can't win a slot at `tip`. |\n| `notes[].note_id` | string | The ID of the note. |\n| `notes[].value` | integer | The value of the note. |\n| `notes[].public_key` | string | The ZK public key that holds the note. |\n| `count` | integer | The number of eligible notes. |\n| `total_value` | integer | The total value of the eligible notes. |\n\n### `GET /leader/claim/vouchers`\n\nGets the reward vouchers the wallet can claim from proposing blocks.\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tip` | string | No | The ID of the block to check at. Defaults to the current tip. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tip` | string | The ID of the block the vouchers were checked at. |\n| `vouchers` | array of objects | The claimable vouchers. Each has a `commitment` and a `nullifier` string. |\n| `reward_amount` | integer | What a single voucher pays out at `tip`. The reward pool is split evenly across every unclaimed voucher on the chain, so this value changes as other leaders claim. |\n| `total_claimable` | integer | `reward_amount` multiplied by the number of `vouchers`. |\n\n### `POST /leader/claim`\n\nClaims one leader reward voucher and adds the claim transaction to the mempool. The request takes no body. To claim several vouchers, call this endpoint once per voucher listed by [`GET /leader/claim/vouchers`](#get-leaderclaimvouchers).\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tx_hash` | string | The hash of the submitted claim transaction. |\n\nReturns `500` if the wallet has no claimable voucher.\n\n## Wallet\n\n### `GET /wallet/{public_key}/balance`\n\nGets the balance of a key in the node's wallet.\n\n#### Path parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `public_key` | string | The ZK public key to check. |\n\n#### Query parameters\n\n| Parameter | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tip` | string | No | The ID of the block to read the balance at. Defaults to the current tip. |\n\n#### Response fields\n\nReturns `404` if the key holds no notes at that block. This includes a key that the node's wallet holds but that hasn't received any funds yet, and a funded key whose funding block the node hasn't synced yet.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tip` | string | The ID of the block the balance was read at. |\n| `balance` | integer | The total value of the key's notes, excluding notes deposited into channels. |\n| `notes` | object | An object that maps each note ID to the note's value. |\n| `address` | string | The ZK public key. |\n\n### `POST /wallet/transactions/transfer-funds`\n\nBuilds a transfer transaction, funds and signs it with the node's wallet, and adds it to the mempool.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `recipient_public_key` | string | Yes | The ZK public key that receives the funds. |\n| `amount` | integer | Yes | The amount to transfer. |\n| `change_public_key` | string | Yes | The ZK public key that receives any UTXO change. |\n| `funding_public_keys` | array of strings | Yes | The ZK public keys whose notes the wallet can spend to fund the transfer and its fee. |\n| `tip` | string | No | The ID of the block to fund the transaction against. Defaults to the current tip. |\n\n#### Response fields\n\nReturns `201` on success.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `hash` | string | The hash of the submitted transaction. |\n\n### `POST /wallet/fund`\n\nAdds inputs and a fee payment to an unsigned transaction, using the node's wallet. The node doesn't sign the transaction's own operations or submit it.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tx_builder` | object | Yes | The unsigned transaction to fund. |\n| `change_public_key` | string | Yes | The ZK public key that receives any UTXO change. |\n| `funding_public_keys` | array of strings | Yes | The ZK public keys whose notes the wallet can spend to pay the fee. |\n| `max_tx_fee` | integer | Yes | The maximum fee the funded transaction can pay. The request fails if the fee is higher. |\n| `priority_fee_percent` | integer | No | An extra fee reserve, as a percentage of the mandatory fee, to absorb fee changes before the transaction is included. `0` funds exactly the mandatory fee. The value isn't capped at `100`. Defaults to `0`. |\n| `tip` | string | No | The ID of the block to fund the transaction against. Defaults to the current tip. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tip` | string | The ID of the block the transaction was funded against. |\n| `funded_tx` | object | The funded transaction, with the fee transfer added as the last operation. All operations are still unsigned. |\n| `transfer_proof` | object or `null` | The proof for the added fee transfer, signed over the funded transaction's hash. `null` if funding needed no transfer. |\n\n### `POST /wallet/sign/ed25519`\n\nSigns a transaction hash with one of the wallet's Ed25519 keys.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tx_hash` | string | Yes | The hash to sign. |\n| `pk` | string | Yes | The Ed25519 public key whose secret key signs the hash. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `sig` | string | The signature. |\n\n### `POST /wallet/sign/zk`\n\nSigns a transaction hash with one or more of the wallet's ZK keys.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `tx_hash` | string | Yes | The hash to sign. |\n| `pks` | array of strings | Yes | The ZK public keys whose secret keys sign the hash. Up to 32 keys. |\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `sig` | object | The ZK signature, as an object with the proof components `pi_a`, `pi_b`, and `pi_c`. |\n\n## Proof-of-Work\n\nThe four `PUT` endpoints used for [EmPoWering](https://docs.logos.co/get-started/glossary.md#empowering) take no request body. On success, each returns status `200` with a `null` JSON body. The response confirms only that the node received the command, so use [`GET /pow/status`](#get-powstatus) to check the resulting state.\n\n### `GET /pow/status`\n\nGets the runtime state of the PoW service: whether the node is [mining](https://docs.logos.co/get-started/glossary.md#mining), whether auto-claim is armed, and the balance of each auto-claim target.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `is_mining` | boolean | Whether mining is turned on. This setting isn't persisted, so a restart clears it. |\n| `are_rewards_enabled` | boolean | Whether PoW rewards are enabled for the node's deployment. Auto-claim can't start while rewards are disabled. |\n| `auto_claim` | object | The runtime state of auto-claim, which claims rewards without operator action. |\n| `auto_claim.is_armed` | boolean | Whether auto-claim is running. |\n| `auto_claim.tick` | object | How often auto-claim tries to claim, as set in the node's `pow.auto_claim.tick` configuration. |\n| `auto_claim.tick.unit` | string | `seconds` to pace claim attempts by wall-clock time, or `slots` to pace them by chain progress. |\n| `auto_claim.tick.value` | integer | The number of seconds or slots between claim attempts. Always greater than zero. |\n| `auto_claim.targets` | array of objects | The keys auto-claim pays, as set in the node's `pow.auto_claim.targets` configuration. Empty if no targets are configured. |\n| `auto_claim.targets[].public_key` | string | The ZK public key the rewards are paid to. |\n| `auto_claim.targets[].threshold` | integer | The balance, in tokens, this key should reach. When the key's on-chain balance is at or above this value, auto-claim stops paying it. |\n| `auto_claim.targets[].balance` | integer or `null` | The key's current balance. `null` if the node couldn't read the balance from the wallet. |\n\n### `GET /pow/rewards/claimable`\n\nGets a summary of the PoW rewards the node can currently claim. Each reward corresponds to a winning ticket the node mined. A ticket can be claimed only while it's within the reward window.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `claimable_tickets` | integer | The number of mined tickets that are still within the reward window. |\n| `slots_until_expiry` | array of integers | One entry per claimable ticket: the number of slots during which the ticket stays within the reward window, before it can no longer be claimed. |\n\n### `PUT /pow/mining/start`\n\nStarts PoW mining. The setting isn't persisted, so the node stops mining when it restarts. While auto-claim is armed, the node also stops mining when every auto-claim target has reached its threshold.\n\n### `PUT /pow/mining/stop`\n\nStops PoW mining.\n\n### `PUT /pow/auto-claim/start`\n\nStarts auto-claim. At every tick, the node pays the claimable rewards to the configured target that holds the lowest balance among those still below their threshold.\n\nThe node ignores this request if PoW rewards are disabled or if no auto-claim targets are configured. Auto-claim stops itself, and stops mining, when every target has reached its threshold.\n\n### `PUT /pow/auto-claim/stop`\n\nStops auto-claim. You can still claim rewards manually with [`POST /pow/claim`](#post-powclaim).\n\n### `POST /pow/claim`\n\nBuilds and submits a transaction that claims the node's currently claimable PoW rewards.\n\n#### Request fields\n\nThe request body is optional.\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `claim_address` | string | No | The ZK public key the rewards are paid to. If you omit this field or the whole body, the node pays the auto-claim target that holds the lowest balance among those still below their threshold. In that case, the request fails if no targets are configured or every target has reached its threshold. |\n\n> **Warning**\n>\n> The node doesn't reject a request body it can't parse. If the body isn't valid JSON, or `claim_address` isn't a valid ZK public key, the node treats the request as if it had no body and pays the auto-claim target instead.\n\n#### Response fields\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `tx_hash` | string or `null` | The hash of the submitted reward-claim transaction. `null` if there were no rewards to claim. |\n\n## Administration\n\n### `PUT /admin/tracing/filter`\n\nReplaces the node's log filter while the node runs.\n\n#### Request fields\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `filters` | object | Yes | An object that maps each log target to a level: `trace`, `debug`, `info`, `warn`, or `error`. Use the `*` key to set the default level for every target. |\n\nFor example:\n\n```json\n{\n  \"filters\": {\n    \"*\": \"info\"\n  }\n}\n```\n\n#### Response\n\nReturns `200` with a `null` body on success.\n"},{"id":"lez/introduction-to-the-logos-execution-zone","title":"Introduction to the Logos Execution Zone","summary":"Understand how the Logos Execution Zone runs general-purpose applications with selective privacy.","section":"lez","doc_type":"concept","url":"https://docs.logos.co/lez","markdown_url":"https://docs.logos.co/lez.md","permalink":"/lez","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/introduction-to-the-logos-execution-zone.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Accounts Model"},{"depth":2,"text":"LEZ Node Architecture"},{"depth":2,"text":"Use Cases"}],"markdown":"# Introduction to the Logos Execution Zone\n\n#### Understand how the Logos Execution Zone runs general-purpose applications with selective privacy.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nThe [Logos Execution Zone](https://docs.logos.co/get-started/glossary.md#logos-execution-zone) ([LEZ](https://docs.logos.co/get-started/glossary.md#lez)) is the primary execution layer for applications built on the Logos stack. It is implemented as a [Zone](https://docs.logos.co/blockchain/concepts/about-zones.md) on the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) and uses zero knowledge proofs to ensure the correctness of its operations. The LEZ runs a Risc0-based virtual machine, the [Logos Execution Environment](https://docs.logos.co/get-started/glossary.md#logos-execution-environment) ([LEE](https://docs.logos.co/get-started/glossary.md#lee)), which separates state into public and private components that LEE programs can use and modify seamlessly. This selective privacy lets developers write generic programs while the LEE enforces privacy and correctness.\n\n## The basics\n\n- The LEZ is the flagship Zone for general-purpose applications on Logos, with built-in support for private execution.\n- The LEZ separates state into public and private accounts. Public executions are validated by re-execution, and private executions are proven with zero knowledge proofs.\n- The LEZ supports applications with high transaction throughput, and inscribes its state updates on the Logos Blockchain through the LEZ [channel](https://docs.logos.co/get-started/glossary.md#channel).\n\n## Accounts Model\n\nTo achieve separation between public and private state while allowing for full programmability, the LEE uses a stateless [program](https://docs.logos.co/get-started/glossary.md#program) model. Under this model, all persistent data is stored in accounts, which can be either public or private. Public accounts are stored on-chain as a map between the [account](https://docs.logos.co/get-started/glossary.md#account) ID and its associated state (such as a token balance). A [public account](https://docs.logos.co/get-started/glossary.md#public-account) owner’s private key signs transactions and authorises executions, while the derived account ID is publicly visible on-chain.\n\nPrivate accounts, by contrast, are stored locally on the account-holder’s node, which publishes [commitments](https://en.wikipedia.org/wiki/Commitment_scheme) to the account state onto the chain whenever the state is updated. The latest commitment binds the current account state to the chain without revealing its data, while nullifiers for previous commitments ensure that old commitments are not used for program execution. Private accounts are created with two associated key pairs.\n\n-  [Nullifier keys](https://docs.logos.co/get-started/glossary.md#nullifier-keys): The private nullifier key is used by the account owner to authorise executions: inside the ZK proof it produces the nullifier that marks the previous commitment as spent. The public nullifier key, together with the public viewing key and an identifier, derives the account ID.\n-  [Viewing keys](https://docs.logos.co/get-started/glossary.md#viewing-keys): The public viewing key is used by anyone updating the account to encrypt its new state for the owner. The private viewing key lets the owner decrypt that state.\n\n> **Info**\n>\n> A program must obtain a [private account](https://docs.logos.co/get-started/glossary.md#private-account)’s [nullifier public key](https://docs.logos.co/get-started/glossary.md#nullifier-public-key), as well as its [viewing public key](https://docs.logos.co/get-started/glossary.md#viewing-public-key), in order to modify a private account state. Therefore, you cannot transfer tokens to a private account without being provided this information by the owner.\n\nWhen an LEE program executes, it modifies the accounts provided to it. These accounts could be either public or private, as an LEE program can be executed over both types of accounts. In either case, the program is executed using the same bytecode, the main difference being whether the user who runs a program will generate a zero knowledge proof. Executions modifying public accounts are executed transparently by LEZ validators like any standard RISC-V VM call. On the other hand, private executions involve the generation of zero knowledge proofs via Risc0, which are then verified by LEZ validators. \n\n## LEZ Node Architecture\n\nThe LEZ operates as a Zone, with updates to its state (both public and private) inscribed on the Logos Blockchain. The LEZ is sequenced in a decentralised manner by a permissioned set of sequencer nodes, who take turns posting the latest updates to the LEZ channel on [Mantle](https://docs.logos.co/blockchain/concepts/about-mantle.md). In addition to the sequencers, the LEZ relies on a network of validators that anyone can join, which update the Zone state by transparently executing programs (for public executions) or by verifying zero knowledge proofs of correct state transitions (for private executions). Anybody can join the validator network and contribute to the correctness of the LEZ state, submitting the latest state to an LEZ sequencer to post on the blockchain. The LEZ also supports read-only indexer nodes that follow the LEZ channel for purposes such as block exploration and development.\n\n## Use Cases\n\nThe LEZ is a ready-made execution environment for privacy-preserving applications built using the Logos stack. Since the same LEE program can be executed over both public and private accounts, computationally costly private executions can be used only when necessary to speed up execution. Together with Solana-style parallel execution, this feature makes the LEZ uniquely suited to applications requiring high throughput as well as private application state.\n\nAs an example, the LEZ could be used by users to run private DeFi applications. With support for privacy built-in, the LEZ could be used to build protocols where transaction details, user balances, or trading strategies are kept private by default, only revealing necessary information to authorised parties. This is especially so when high volumes of transactions are expected by these applications. Examples may include:\n\n- Private Exchanges: Trading platforms where order books or individual trades are obscured.\n- Private Lending/Borrowing Protocols: Financial services where loan details or collateral information are private.\n- Private Transfers: Securely sending and receiving digital assets with transaction details kept private using zero knowledge proofs, ensuring financial privacy for individuals and businesses.\n"},{"id":"lez/get-started/run-lez-wallet-app-initiate-native-token-transfers","title":"Run the LEZ wallet app and initiate native token transfers","summary":"Use the wallet app to set up accounts and try every type of native token transfer on the LEZ testnet.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers","markdown_url":"https://docs.logos.co/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md","permalink":"/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/get-started/run-lez-wallet-app-initiate-native-token-transfers.md","headings":[{"depth":2,"text":"What to expect"},{"depth":3,"text":"Run the wallet app in Basecamp"},{"depth":2,"text":"Set up and sync the wallet app"},{"depth":2,"text":"Create a new public account"},{"depth":2,"text":"Send a public transfer"},{"depth":2,"text":"Verify L1 finalisation"},{"depth":2,"text":"Try private, shielded, and deshielded transfers"},{"depth":2,"text":"Withdraw tokens to the L1"},{"depth":2,"text":"Troubleshooting wallet app setup"},{"depth":3,"text":"Why doesn't the setup screen show?"},{"depth":3,"text":"Why does creating a wallet fail with `A wallet already exists at`?"}],"markdown":"# Run the LEZ wallet app and initiate native token transfers\n\n#### Use the wallet app to set up accounts and try every type of native token transfer on the LEZ testnet.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThe wallet app is a simple entrypoint for getting started on the [LEZ](https://docs.logos.co/get-started/glossary.md#lez). This procedure walks you through running the wallet app, syncing it with the public LEZ testnet, creating [accounts](https://docs.logos.co/get-started/glossary.md#account), executing all combinations of public, private, shielded, and deshielded native token transfers, and withdrawing tokens to the L1 through the bridge. The LEZ testnet runs the centralised LEZ sequencer, which processes the transactions the wallet app submits, while the wallet app itself manages your accounts locally and can execute transfers with any combination of private and public accounts.\n\nThis page describes the **LEZ Wallet** [app](https://docs.logos.co/get-started/glossary.md#app) version 1.2.0 (`lez_wallet_ui`), which requires the `lez_core` module version 0.5.0 or later.\n\n> **Note**\n>\n> Recovering a wallet from its recovery phrase is not yet supported.\n\n> **Info: Prerequisites**\n>\n> - [Logos Basecamp installed and running](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - macOS (Apple silicon) or Linux (x86-64 or ARM64) only.\n\n## What to expect\n\n- You can run the wallet app in Logos Basecamp and sync it with the live LEZ testnet.\n- You can create and name public and private accounts, and fund them to test transfers.\n- You can complete public, private, shielded, and deshielded token transfers, verify finalisation on the L1, and withdraw tokens to the L1.\n\n### Run the wallet app in Basecamp\n\n1. At the bottom of the sidebar, click **Applications**.\n1. Select `Wallet` in **Categories**, or search for `LEZ Wallet`, then click the **LEZ Wallet** card.\n1. In the **Add Application** dialogue, click **Install**. **Install** also installs the `lez_core` [module](https://docs.logos.co/get-started/glossary.md#module) listed under **Required Packages**.\n1. Wait until the button changes to **Launch**, then click it. A **LEZ Wallet** icon appears in the sidebar, so you can reopen the wallet from there later.\n\nTo install [packages](https://docs.logos.co/get-started/glossary.md#package) one by one instead, see [Install and load a module in Basecamp](https://docs.logos.co/basecamp/install-and-load-module-in-basecamp.md).\n\n## Set up and sync the wallet app\n\nThis task creates your wallet and syncs it with the LEZ testnet.\n\n1.  On the **LEZ Wallet** welcome screen, click **Set up your wallet**.\n\n    ![The LEZ Wallet welcome screen](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/setup-welcome.png)\n\n1.  Follow the four steps of the **Set up your wallet** wizard:\n\n    1.  **Setup**: select **Create a new wallet** and click **Continue**. To reuse wallet files you already have, select **I already have a wallet** instead, choose your **Storage file** and **Config file**, and click **Open Wallet**.\n\n        ![Step 1. Setup of the Set up your wallet wizard](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/setup-wizard1.png)\n\n    1.  **Network**: keep the **Testnet** preset (`https://testnet.lez.logos.co`) and click **Continue**.\n\n        ![Step 2. Network with the Testnet preset](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/setup-wizard-network.png)\n\n    1.  **Password**: enter a password in both fields and click **Create Wallet**. The password encrypts the wallet on this machine; it is not your recovery phrase.\n\n        ![Step 3. Password](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/setup-wizard-password.png)\n\n    1.  **Keys**: write down the 24-word recovery phrase, which is shown only once, tick **I've written down my recovery phrase and stored it safely**, and click **Open wallet**.\n\n        ![Step 4. Keys, with the recovery phrase blurred](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/setup-backup-keys.png)\n\n1.  Wait for the wallet app to sync with the testnet. The wallet finds your [private account](https://docs.logos.co/get-started/glossary.md#private-account) values by scanning blocks, so accounts and balances may be missing until the sync finishes.\n\n    While it syncs, the **Accounts** panel shows **Syncing** with the number of blocks processed and a progress bar. This can take a few minutes. When the sync finishes, the panel briefly shows **Up to date · block** followed by the current block height.\n\n    ![The Accounts panel while the wallet syncs](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/sync-progress.png)\n\n    ![The Accounts panel after the sync, with one public and one private account](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/sync-done.png)\n\n    To sync again and reload balances later, click **Refresh** in the **Accounts** panel.\n\n## Create a new public account\n\nA new wallet starts with one public account and one private account, both empty. Accounts you create are controlled only by you, and you need a funded account before you can send a transfer.\n\n1.  In the **Accounts** panel, click **+ Create** next to **Public Accounts**. If your account list is empty, click **+ Public account** instead.\n\n    The new [public account](https://docs.logos.co/get-started/glossary.md#public-account) appears in the account list with its account ID and balance in TestToken. The edit (pencil) and copy buttons sit on the right of each account.\n\n    ![A public account with its edit and copy buttons highlighted](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/account-actions.png)\n\n1.  Optional: to name the account, click the edit (pencil) button next to it, enter a name in the **Add name** dialogue, and click **Save**. Each name must be unique in the wallet, and the edit button shows only on accounts that don't have a name yet.\n\n1.  Fund the account. Next to the account, click the copy button to copy its account ID, then have another testnet user send TestToken to it, or [bridge tokens from the L1](https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md).\n\n## Send a public transfer\n\nThis task moves tokens from a funded public account to another public account.\n\n1. In the transfer panel, select **Send**, then select the **Public** tab.\n1. In the **From** field, choose a public account with a positive balance.\n1. In the **To** field, either paste the recipient's account ID, or tick **Use owned account** and choose one of your own public accounts.\n1. In **Amount (TestToken)**, enter the amount to send.\n1.  Click **Send**.\n\n    A result line displays below the **Send** button and reads `Success. Tx:` followed by the transaction hash. Use its copy button to copy the hash for the next task.\n\n## Verify L1 finalisation\n\n1.  In your browser, navigate to `https://explorer.testnet.lez.logos.co/transaction/<TX_HASH>`, replacing `<TX_HASH>` with the transaction hash from the previous task.\n\n    The explorer shows only LEZ blocks that are finalised on the L1, so the transaction details display about an hour after the transfer. Until then, the page cannot find the transaction. You can also paste the hash in the search box on the [LEZ block explorer](https://explorer.testnet.lez.logos.co/) home page.\n\n    ![Transaction details on the explorer](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/run-lez-wallet-ui-and-initiate-native-token-transfers/tx-finalized.png)\n\n## Try private, shielded, and deshielded transfers\n\nRepeat the create-account and transfer tasks for the other account types and transfer flows. Each of these transfers generates a proof, and the transfer panel shows **Generating proof, please wait…** until it finishes.\n\n1. Create one or more private accounts: click **+ Create** next to **Private Accounts**, or **+ Private account** if your account list is empty.\n1. On the **Private** tab, send a private transfer from a private sender to a private recipient.\n1. On the **Shielded** tab, send a shielded transfer from a public sender to a private recipient.\n1. On the **Deshielded** tab, send a deshielded transfer from a private sender to a public recipient.\n\nEach transfer shows `Success. Tx:` followed by its transaction hash below the button. To check its status, look it up on the explorer as in [Verify L1 finalisation](#verify-l1-finalisation), and expect the same delay of about an hour.\n\nTo send to one of your own accounts, tick **Use owned account** and choose the recipient. To send to a private account in another wallet, paste the recipient's public keys as JSON with `nullifier_public_key` and `viewing_public_key`. To share your own private keys, click the copy button next to **Accounts under keys** in the **Private Accounts** section.\n\n## Withdraw tokens to the L1\n\nThe **Bridge** section withdraws native tokens from one of your public accounts to a public key on the L1 (Bedrock). Only public accounts can send bridge withdrawals.\n\n1. In the transfer panel, select **Bridge**.\n1. In the **From** field, choose a public account with a positive balance.\n1. In **Bedrock (L1) public key**, paste the recipient's Bedrock public key as hex.\n1. In **Amount (TestToken)**, enter the amount to withdraw.\n1.  Click **Withdraw**.\n\n    A result line displays below the **Withdraw** button. On success, it reads `Success. Tx:` followed by the transaction hash.\n\n## Troubleshooting wallet app setup\n\n### Why doesn't the setup screen show?\n\nThe setup screen is skipped if the settings saved by the wallet app already list paths for the storage and config files, typically from an earlier run of the wallet app, and the wallet at those paths opens. If the saved files no longer exist, the wallet app forgets them and shows the setup screen.\n\nThe settings are stored under the organisation `Logos` and application `ExecutionZoneWalletUI`, whether the wallet runs in Basecamp or on its own. To see the setup screen again, quit the wallet app and remove the settings:\n\n- **Linux**: remove `~/.config/Logos/ExecutionZoneWalletUI.conf`.\n- **macOS**: run the following commands:\n\n    ```bash\n    defaults delete com.logos.ExecutionZoneWalletUI 2>/dev/null\n    rm -f ~/Library/Preferences/com.logos.ExecutionZoneWalletUI.plist\n    killall cfprefsd\n    ```\n\n### Why does creating a wallet fail with `A wallet already exists at`?\n\nThe wallet UI won't create a new wallet over existing wallet files. The error message names the existing storage file. To keep using that wallet, select **I already have a wallet** in the **Setup** step and choose that storage file and the `config.json` file in the same folder. To create a new wallet instead, move or remove those files first.\n"},{"id":"lez/get-started/run-lez-wallet-via-cli","title":"Run an LEZ wallet via the CLI","summary":"Try the wallet CLI against the live LEZ testnet.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli","markdown_url":"https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md","permalink":"/lez/get-started/run-lez-wallet-via-cli","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/get-started/run-lez-wallet-via-cli.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Install the wallet and connect it to the testnet"},{"depth":2,"text":"Verify the connection"},{"depth":2,"text":"Complete a minimal wallet flow"},{"depth":3,"text":"Create and initialise the sender public account"},{"depth":3,"text":"Fund the sender account"},{"depth":3,"text":"Create and fund the recipient public account"},{"depth":2,"text":"Next steps"}],"markdown":"# Run an LEZ wallet via the CLI\n\n#### Try the wallet CLI against the live LEZ testnet.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure explains how to install the wallet CLI from the [LEZ repository](https://github.com/logos-blockchain/logos-execution-zone/) and point it at the public [LEZ](https://docs.logos.co/get-started/glossary.md#lez) testnet sequencer.\n\n> **Info: Prerequisites**\n>\n>\n> - **Rust and `cargo`**—install with [`rustup`](https://rustup.rs).\n>    - The wallet pins its toolchain via `rust-toolchain.toml`, so the correct version is selected automatically.\n> - **System build dependencies** for compiling the wallet from source:\n>\n>    ```bash\n>   # Ubuntu / Debian\n>   sudo apt update\n>   sudo apt install git curl build-essential clang libclang-dev pkg-config libssl-dev libpcsclite-dev\n>\n>   # Fedora\n>   sudo dnf install git curl gcc glibc-devel clang clang-devel pkgconf-pkg-config openssl-devel llvm-libs pcsc-lite-devel\n>   \n>   # macOS\n>   xcode-select --install\n>   brew install pkg-config openssl\n>   ```\n\n## What to expect\n\n- You can run wallet commands against the live LEZ testnet sequencer.\n- Your wallet is configured to target `https://testnet.lez.logos.co` as the sequencer address.\n\n## Install the wallet and connect it to the testnet\n\n1. In the LEZ repository, check out the correct release tag:\n\n   ```sh\n   git clone https://github.com/logos-blockchain/logos-execution-zone.git\n   cd logos-execution-zone\n   git checkout v0.3.0\n   ```\n\n   - The tag must match the programs the testnet runs.\n\n1. Rename the existing wallet directory (if you have one) to avoid conflicts:\n\n   ```sh\n   mv ~/.lee/wallet ~/.lee/wallet.old 2>/dev/null || true\n   ```\n\n1. Install the wallet CLI:\n\n   ```sh\n   cargo install --path lez/wallet --force\n   ```\n\n1. Point the wallet at the testnet:\n\n   ```sh\n   wallet change-network testnet\n   ```\n\n## Verify the connection\n\n1. Run the health check command:\n\n   ```sh\n   wallet check-health\n   ```\n\n   A successful connection returns:\n\n   ```sh\n   ✅All looks good!\n   ```\n\n## Complete a minimal wallet flow\n\nIn this flow, you create an [account](https://docs.logos.co/get-started/glossary.md#account), claim testnet funds, send a transfer, and confirm resulting balances.\n\nIn this task, wallet account and transfer commands interact with the `authenticated-transfer` [program](https://docs.logos.co/get-started/glossary.md#program), and sequencer processing determines the resulting account state. Public and [private account](https://docs.logos.co/get-started/glossary.md#private-account) paths share command patterns, while private paths can include local proof generation.\n\n### Create and initialise the sender public account\n\n1. Create a sender [public account](https://docs.logos.co/get-started/glossary.md#public-account) and record the `account_id` value:\n\n   ```bash\n   wallet account new public\n   ```\n\n1. Using the sender public `account_id` from the previous step, check the sender status:\n\n   ```bash\n   wallet account get --account-id <sender_public_account_id>\n   ```\n\n   Example:\n\n   ```bash\n   wallet account get --account-id Public/14TYHiuzKiNR1ydETpr9mJMkjY6jf1hQFZ11d3X8Tc7N\n   ```\n\n   You should see `Balance 0, nonce 0` in the output. New accounts start empty and not owned, and you don't need to initialise them: the `authenticated-transfer` program can credit native tokens to any account.\n\n### Fund the sender account\n\n1. Send native tokens to the sender account in one of these ways:\n\n   - Bridge tokens from the Logos Blockchain with a [channel deposit](https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md). In **Metadata**, paste the sender account ID without its `Public/` prefix.\n   - Have another testnet user transfer native tokens to the sender account ID.\n\n1. Check the sender account balance:\n\n   ```bash\n   wallet account get --account-id <sender_public_account_id>\n   ```\n\n   In the output, the number after `Balance` should be greater than `0`.\n\n### Create and fund the recipient public account\n\n1. Create a recipient public account and record the `account_id` value:\n\n   ```bash\n   wallet account new public\n   ```\n\n1. Send 37 tokens from sender to recipient:\n\n   ```bash\n   wallet auth-transfer send \\\n       --from <sender_public_account_id> \\\n       --to <recipient_public_account_id> \\\n       --amount 37\n   ```\n\n   Example:\n\n   ```bash\n   wallet auth-transfer send \\\n       --from Public/14TYHiuzKiNR1ydETpr9mJMkjY6jf1hQFZ11d3X8Tc7N \\\n       --to Public/74zHyMW81mtfcd6VMaLnpnAna8k2V4AN2Ygyy9LcEAQQ \\\n       --amount 37\n   ```\n\n1. Check sender and recipient balances:\n\n   ```bash\n   # Sender account\n   wallet account get --account-id <sender_public_account_id>\n   ```\n\nThe sender's `Balance` should be `37` lower than before the transfer.\n\n   ```bash\n   # Recipient account\n   wallet account get --account-id <recipient_public_account_id>\n   ```\n\nThis should show `Balance 37`.\n\n## Next steps\n\n- [Transfer native tokens on the LEZ](https://docs.logos.co/lez/transfer-tokens/transfer-native-tokens-on-lez.md)\n- [Create and transfer custom tokens on the LEZ](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md)\n"},{"id":"lez/transfer-tokens/transfer-native-tokens-on-lez","title":"Transfer native tokens on the LEZ","summary":"Use the wallet CLI to send native tokens to public and private accounts.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/transfer-tokens/transfer-native-tokens-on-lez","markdown_url":"https://docs.logos.co/lez/transfer-tokens/transfer-native-tokens-on-lez.md","permalink":"/lez/transfer-tokens/transfer-native-tokens-on-lez","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/transfer-tokens/transfer-native-tokens-on-lez.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Create and fund a sender account"},{"depth":2,"text":"Step 2: Transfer tokens"},{"depth":3,"text":"Method 1: Transfer tokens using the recipient account ID"},{"depth":3,"text":"Method 2: Transfer tokens using the recipient account `npk` and `vpk`"},{"depth":2,"text":"Step 3: Verify the transfer"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"Symptom"},{"depth":3,"text":"Explanation"}],"markdown":"# Transfer native tokens on the LEZ\n\n#### Use the wallet CLI to send native tokens to public and private accounts.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThe Logos Execution Zone ([LEZ](https://docs.logos.co/get-started/glossary.md#lez)) is a programmable blockchain that cleanly separates public and private state while keeping them fully interoperable. It's a component of the Logos project. You can use the wallet CLI to invoke LEZ's authenticated-transfers [program](https://docs.logos.co/get-started/glossary.md#program) to transfer native tokens between public and private accounts.\n\nOn LEZ, public and private accounts differ in where their state lives and how transfers update that state.\n\n- Public accounts\n  - Live on-chain.\n  - Identified by a 32-byte [account](https://docs.logos.co/get-started/glossary.md#account) ID derived from the public key\n  - The private key signs transactions and authorises program executions.\n- Transfers between public accounts\n  - Validators execute the authenticated-transfers program transparently.\n  - The public state (the on-chain map from [public account](https://docs.logos.co/get-started/glossary.md#public-account) IDs to their account states) is updated in place by debiting the sender's public account and crediting the recipient's public account.\n- Private accounts\n  - Structurally identical to public accounts, but their values are stored off-chain.\n  - Use two keypairs: [nullifier keys](https://docs.logos.co/get-started/glossary.md#nullifier-keys) for privacy-preserving executions and [viewing keys](https://docs.logos.co/get-started/glossary.md#viewing-keys) for encrypting and decrypting values.\n  - The [private account](https://docs.logos.co/get-started/glossary.md#private-account) ID is derived from the [nullifier public key](https://docs.logos.co/get-started/glossary.md#nullifier-public-key), the viewing public key, and an identifier.\n  - Anyone can initialise private accounts, but once initialised they can only be modified by the owner's keys.\n- Transfers involving any private account\n  - The execution is privacy-preserving.\n  - The transfer generates a zero-knowledge proof of correct execution locally, and submits the proof for validators to verify. Each update to the private state produces a new commitment (a 32-byte, hash-like binding to the actual values), and the previous commitment is marked as spent via a nullifier set so only the latest version can be used, while the actual private values remain local to the account owner.\n  - Any public accounts involved are still updated visibly on-chain.\n\n> **Danger**\n>\n> Transfers are irreversible. Double-check all details before proceeding.\n\n> **Info: Prerequisites**\n>\n>\n> - An [LEZ CLI wallet](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md) set up and funded.\n\n## What to expect\n\n- The authenticated-transfers program manages native token transfers and enforces authenticated debits. When making transfers, you use the wallet CLI to interact with the program.\n  - Unlike the [token program](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md), the authenticated-transfers program is built into LEZ rather than published in [`lez-programs`](https://github.com/logos-blockchain/lez-programs) with an IDL, so the [SPEL CLI](https://github.com/logos-co/spel) can't drive it.\n- You can initialise accounts by sending tokens to them. The authenticated-transfers program claims any uninitialised account used in a transfer.\n- You can transfer native tokens to public accounts and verify balances on-chain.\n- Your private account balances are in your local wallet storage and rely on zero-knowledge proofs for privacy.\n\n## Step 1: Create and fund a sender account\n\n1.  Create a public account or private account according to your needs.\n\n    - Public account:\n\n    ```sh\n    wallet account new public\n    ```\n\n    - Private account:\n\n    ```sh\n    wallet account new private\n    ```\n\n    If you create a public account, the output is the account ID. If you create a private account, the output includes the account ID, nullifier public key (`npk`), and [viewing public key](https://docs.logos.co/get-started/glossary.md#viewing-public-key) (`vpk`).\n\n    > **Info**\n    >\n    > Your account keys and data are stored in the local file `$HOME/.lee/wallet/storage.json`.\n\n1.  Use the `wallet account ls` command to confirm the accounts are created successfully. You should see a list showing all of your accounts.\n\n    ```sh\n    wallet account ls\n    ```\n    \n1.  Fund the sender account. Bridge tokens from the Logos Blockchain with a [channel deposit](https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md), pasting the account ID without its `Public/` prefix in **Metadata**, or have another testnet user transfer native tokens to it. A deposit can only fund a public account.\n1.  Confirm your account balance after funding using the `wallet account get` command:\n\n    ```sh\n    wallet account get --account-id ACCOUNT-TYPE/ACCOUNT-ID\n    ```\n\n    The output shows `Balance` followed by a number greater than `0`.\n\n## Step 2: Transfer tokens\n\nWhen transferring native tokens using the `wallet auth-transfer send` command, you specify the sender account with its account ID.\n\nThere are two ways to specify the recipient account depending on the account type:\n\n- [Method 1: Use the recipient account ID](https://docs.logos.co/lez/transfer-tokens/transfer-native-tokens-on-lez.md#method-1-transfer-tokens-using-the-recipient-account-id)\n- [Method 2: Use the recipient account `npk` and `vpk`](https://docs.logos.co/lez/transfer-tokens/transfer-native-tokens-on-lez.md#method-2-transfer-tokens-using-the-recipient-account-npk-and-vpk)\n\nWith the recipient account ID, you can transfer native tokens across the following account types:\n\n- Public → public (yours or someone else's)\n- Private → public (yours or someone else's)\n- Public → your private\n- Private → your private\n\n> **Info**\n>\n> Transfers involving private accounts may take a few minutes because the wallet needs to generate a local proof.\n\nWith the `npk` and `vpk` of the recipient account, you can transfer native tokens across the following account types:\n\n- Public → uninitialised private (someone else's)\n- Private → uninitialised private (someone else's)\n\n> **Info**\n>\n> Currently, only uninitialised private accounts can be modified without authorisation. Sending funds to initialised private accounts is not possible because only the owner can modify them.\n\n### Method 1: Transfer tokens using the recipient account ID\n\nUse the `wallet auth-transfer send` to transfer tokens. Replace `ACCOUNT-TYPE` with the type of the account (public or private) and `TOKEN-AMOUNT` with the amount of tokens to transfer.\n\n```sh\nwallet auth-transfer send \\\n    --from ACCOUNT-TYPE/SENDER-ACCOUNT-ID \\\n    --to ACCOUNT-TYPE/RECIPIENT-ACCOUNT-ID \\\n    --amount TOKEN-AMOUNT\n```\n\nFor example, to transfer 17 tokens from the public account with ID `Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS` to the private account with ID `HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL`, you run:\n\n```sh\nwallet auth-transfer send \\\n    --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \\\n    --to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \\\n    --amount 17\n```\n\n### Method 2: Transfer tokens using the recipient account `npk` and `vpk`\n\nWhen transferring someone else's private account, you need their account `npk` (nullifier public key)and `vpk` (viewing public key) instead of the account ID because LEZ uses the `npk` to confirm the ownership of the private account, and `vpk` to verify transactions without revealing the account owner. These keys do not expose sensitive information and allow others to verify transaction validity.\n\n> **Tip**\n>\n> Check your account `npk` and `vpk` using the `wallet account get --account-id ACCOUNT-TYPE/ACCOUNT-ID` command.\n\n1.  Use the `wallet auth-transfer send` to transfer tokens to another user's private account. Replace `ACCOUNT-TYPE` with the type of the sender account (public or private) and `TOKEN-AMOUNT` with the amount of tokens to transfer.\n\n    ```sh\n    wallet auth-transfer send \\\n        --from ACCOUNT-TYPE/SENDER-ACCOUNT-ID \\\n        --to-npk RECIPIENT-NPK \\\n        --to-vpk RECIPIENT-VPK \\\n        --amount TOKEN-AMOUNT\n    ```\n\n1.  Once the transaction is accepted, run the following command to scan the chain for encrypted values in the transaction and update the local state accordingly.\n\n    ```sh\n    wallet account sync-private\n    ```\n\n## Step 3: Verify the transfer\n\nConfirm the transfer by checking the balances of both accounts using the `wallet account get` command.\n\n```sh\nwallet account get --account-id ACCOUNT-TYPE/ACCOUNT-ID\n```\n\nFor example, to check the balance of the private account with ID `HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL`, you run:\n\n```sh\nwallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL\n```\n\nThe output looks like this:\n\n```text\nBalance BALANCE-AMOUNT, nonce NONCE\n```\n\n> **Tip**\n>\n> When checking the balance of a private account, the `wallet account get` command doesn't query the network. It works offline because private account data lives only in your wallet storage. Other users cannot read your private balances using this command and your private account ID.\n\n> **Tip**\n>\n> You can also use the `wallet account ls -l` command to check the balances of all your accounts at once.\n\n## Troubleshooting\n\n### Symptom\n\nThe wallet command panics and fails during proof generation with an assertion error when you try to send native tokens to an initialised private account.\n\nThe error message looks like this:\n\n```\nthread 'main' panicked at .../privacy_preserving_circuit/src/output.rs:58:17:\nassertion `left == right` failed: Found new private account with non default values\n  left: Account { ... }\n  right: Account { ... }\n...\ncalled `Result::unwrap()` on an `Err` value: CircuitProvingError(\"Guest panicked: assertion `left == right` failed: Found new private account with non default values ...\")\n```\n\n### Explanation\n\nThe transfer fails because the privacy-preserving circuit expects the recipient private account to be uninitialised (default state), but an initialised private account already contains non-default values and can only be modified by its owner.\n"},{"id":"lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez","title":"Create and transfer custom tokens on the LEZ","summary":"Use the wallet and SPEL CLIs to create custom tokens, transfer them, mint or burn supply, and rotate the mint authority.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez","markdown_url":"https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md","permalink":"/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install `spel`"},{"depth":2,"text":"Step 2: Create a fungible token"},{"depth":2,"text":"Step 3: Give someone a holding"},{"depth":2,"text":"Step 4: Transfer"},{"depth":2,"text":"Step 5: Mint more supply"},{"depth":3,"text":"Option A—Self authority"},{"depth":3,"text":"Option B—External authority"},{"depth":2,"text":"Step 6: Rotate or renounce the mint authority"},{"depth":3,"text":"Option A—Current authority is the definition account"},{"depth":3,"text":"Option B—Current authority is a separate account"},{"depth":2,"text":"Step 7: Burn"},{"depth":2,"text":"Step 8: NFTs"},{"depth":2,"text":"Step 9: Inspect a token account"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"`Transaction NOT confirmed` on a transaction that actually landed"},{"depth":3,"text":"`Token definition must be owned by token program`"},{"depth":3,"text":"A transfer or mint rejects a fresh recipient account"},{"depth":3,"text":"`Option` or account arguments are rejected as missing"},{"depth":3,"text":"`--dry-run` submits the transaction anyway"},{"depth":3,"text":"`spel` rejects an account id argument"}],"markdown":"# Create and transfer custom tokens on the LEZ\n\n#### Use the wallet and SPEL CLIs to create custom tokens, transfer them, mint or burn supply, and rotate the mint authority.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.2.4**.\n\nThe Logos Execution Zone ([LEZ](https://docs.logos.co/get-started/glossary.md#lez)) is a programmable blockchain that cleanly separates public and private state while keeping them fully interoperable. LEZ's [token program](https://docs.logos.co/get-started/glossary.md#token-program) is a single, shared [program](https://docs.logos.co/get-started/glossary.md#program) that creates and manages custom tokens—there's no separate contract deployment per token. You drive it with the [SPEL CLI](https://github.com/logos-co/spel) (`spel`), which sends the program's instructions directly, and the `wallet` CLI, which creates and lists the accounts those instructions use.\n\nToken program accounts fall into three types:\n\n- [Token definition account](https://docs.logos.co/get-started/glossary.md#token-definition-account)\n  - Each token has exactly one token definition account.\n  - It defines the token globally: its name, total supply, optional metadata reference, and optional mint authority.\n  - Its address is the token identifier (similar to a mint address).\n- [Token holding account](https://docs.logos.co/get-started/glossary.md#token-holding-account)\n  - Any [account](https://docs.logos.co/get-started/glossary.md#account) that holds a balance of a token is called a token holding account for that token.\n  - Each token can have multiple token holding accounts.\n- Token metadata account\n  - Created alongside a non-fungible token's definition to hold its standard, URI, and creators.\n\n> **Danger**\n>\n> Transfers, mints, and burns are irreversible, and renouncing the mint authority can never be undone. Double-check all details before submitting a transaction.\n\n> **Info: Prerequisites**\n>\n> - An [LEZ CLI wallet](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md) set up and funded.\n> - The token program's IDL. It's committed at `artifacts/token-idl.json` in the [`lez-programs`](https://github.com/logos-blockchain/lez-programs) repository, so cloning the repository is enough—you don't need to build anything to get it.\n\n## What to expect\n\n- You can create a fungible token with a fixed supply, a self-managed mint authority, or a mint authority delegated to another account.\n- You can transfer, mint, and burn tokens, and rotate or permanently renounce a token's mint authority.\n- You can inspect any token definition, holding, or metadata account to confirm the result of an instruction.\n\n## Step 1: Install `spel`\n\n`spel` is a developer CLI tool used to help build and drive programs that run on the LEZ.\n\n1. Install `spel`.\n\n   \n\n## Step 2: Create a fungible token\n\nCreating a token creates its [token definition account](https://docs.logos.co/get-started/glossary.md#token-definition-account) and its first [token holding account](https://docs.logos.co/get-started/glossary.md#token-holding-account), credited with the full total supply, in one instruction. Both target accounts must be fresh (never written to) and must be accounts your wallet holds the keys for.\n\n1. Create two accounts: one for the token definition, one for the first holding.\n\n   ```sh\n   wallet account new public --label \"Token A Definition\"\n   wallet account new public --label \"Token A Holding\"\n   ```\n\n1. Confirm the accounts were created, and note their ids.\n\n   ```sh\n   wallet account list\n   ```\n\n   - The listing shows each account's label and base58 id, for example `Public/CER21z16YgmWr3aN8FEHsrmfm2iRfQiwZTac3FQa21US [Token A Definition]`. Use those ids wherever this procedure refers to `<DEF>`, `<HOLDING>`, or `<AUTHORITY>`.\n\n1. Create the token with `new-fungible-definition`, replacing `<TOKEN_PROGRAM_ID>` with the `token` row's **ImageID (hex)** value—the 64-character hex string—from [DEPLOYMENTS.md](https://github.com/logos-blockchain/lez-programs/blob/main/DEPLOYMENTS.md). `spel --program` takes only a 64-char hex ID, a name from `spel.toml`, or a path to a program binary, so the base58 ImageID in the next column is read as a file path and fails.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- new-fungible-definition \\\n        --name \"TOKEN A\" \\\n        --total-supply 1000000000000000000000 \\\n        --definition-target-account <DEF> \\\n        --holding-target-account <HOLDING> \\\n        --mint-authority <AUTHORITY>\n   ```\n\n   - `--total-supply` is a `u128` in **base units**—the token program stores no decimals field, so `1000000000000000000000` is 1000 whole tokens only if your UI assumes 18 decimals.\n   - `--mint-authority` is always required (`spel` has no optional flags) and decides the token's supply model:\n\n     | Value | Meaning |\n     |---|---|\n     | `<DEF>` (the definition's own id) | Self authority—mint later with `mint` ([Step 5](#step-5-mint-more-supply)), signing as the definition account. |\n     | Another account id | External authority—mint later with `mint-with-authority` ([Step 5](#step-5-mint-more-supply)), signing as that account. |\n     | `none` | Fixed supply—minting is permanently rejected. |\n\n   - An all-zero authority id is rejected. Both target accounts must be fresh—re-running the command against the same accounts fails.\n\n1. Confirm the definition and holding were created correctly.\n\n   ```sh\n   spel --idl artifacts/token-idl.json inspect <DEF>     --type TokenDefinition\n   spel --idl artifacts/token-idl.json inspect <HOLDING> --type TokenHolding\n   ```\n\n## Step 3: Give someone a holding\n\nBefore an account can receive a transfer or a mint, it needs an initialised [token holding account](https://docs.logos.co/get-started/glossary.md#token-holding-account) for that token's definition. `initialize-account` creates an empty holding—`Fungible { balance: 0 }` for a fungible definition.\n\n1. Create the account that will hold the token.\n\n   ```sh\n   wallet account new public --label \"Token A Holding (Bob)\"\n   ```\n\n1. Initialise it against the token's definition.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- initialize-account \\\n        --definition-account <DEF> \\\n        --account-to-initialize <NEW_HOLDING>\n   ```\n\n   - `<DEF>` is read-only here and isn't signed—anyone can initialise a holding for any definition, as long as they sign the new holding account.\n   - The definition must be owned by the token program build you're using, or the call is rejected.\n\n## Step 4: Transfer\n\n`transfer` moves a balance between two holdings of the **same** definition. `spel` signs the sender; the recipient is only written to, so it must already be initialised ([Step 3](#step-3-give-someone-a-holding))—a fresh account's claim can't be authorised here because `transfer` never collects the recipient's signature.\n\n1. Transfer tokens from a holding you own to an already-initialised recipient holding of the same definition.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- transfer \\\n        --sender <SENDER_HOLDING> \\\n        --recipient <RECIPIENT_HOLDING> \\\n        --amount-to-transfer <AMOUNT>\n   ```\n\n   - `<SENDER_HOLDING>` must be a holding you own, with a balance ≥ `<AMOUNT>`.\n   - `<RECIPIENT_HOLDING>` must already be initialised and belong to the same token definition as the sender, or the transfer is rejected.\n\n1. Confirm the recipient's new balance.\n\n   ```sh\n   spel --idl artifacts/token-idl.json inspect <RECIPIENT_HOLDING> --type TokenHolding\n   ```\n\n## Step 5: Mint more supply\n\nMinting adds to both the holding's balance and the definition's total supply. It's fungible-only, and which instruction you use depends on where the definition's mint authority lives ([Step 2](#step-2-create-a-fungible-token)). The target holding must already be initialised—it isn't a signer, so a fresh account can't be claimed by a mint.\n\n### Option A—Self authority\n\nUse this when the definition's stored mint authority **is the definition account itself**.\n\n1. Mint, signing with the definition account.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- mint \\\n        --definition-account <DEF> \\\n        --user-holding-account <HOLDING> \\\n        --amount-to-mint <AMOUNT>\n   ```\n\n### Option B—External authority\n\nUse this when a **separate account** holds the mint authority—the normal case when you passed a distinct `--mint-authority` at creation, or rotated it later ([Step 6](#step-6-rotate-or-renounce-the-mint-authority)).\n\n1. Mint, signing with the authority account.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- mint-with-authority \\\n        --definition-account <DEF> \\\n        --user-holding-account <HOLDING> \\\n        --authority-account <AUTHORITY> \\\n        --amount-to-mint <AMOUNT>\n   ```\n\nUsing the variant that doesn't match where the authority actually lives fails with `signer is not the current authority`, even if you hold the right key. A revoked (`none`) authority rejects both variants with `authority revoked, supply is fixed`.\n\n## Step 6: Rotate or renounce the mint authority\n\nRotating or renouncing follows the same self-vs-external split as minting, and is fungible-only.\n\n### Option A—Current authority is the definition account\n\n1. Set a new authority, signing with the definition account.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- set-authority \\\n        --definition-account <DEF> \\\n        --new-authority <NEW_AUTHORITY>\n   ```\n\n### Option B—Current authority is a separate account\n\n1. Set a new authority, signing with the current authority account.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- set-authority-with-authority \\\n        --definition-account <DEF> \\\n        --authority-account <CURRENT_AUTHORITY> \\\n        --new-authority <NEW_AUTHORITY>\n   ```\n\n> **Danger**\n>\n> Passing `--new-authority none` **permanently renounces** minting—the supply becomes fixed and no later rotation is possible. There is no undo.\n\n1. Confirm the new authority.\n\n   ```sh\n   spel --idl artifacts/token-idl.json inspect <DEF> --type TokenDefinition\n   ```\n\n## Step 7: Burn\n\n`burn` destroys supply from a holding you own. The holding signs; the definition is written but not signed, so any holder can burn their own tokens without the issuer's involvement, and burning is **not** gated on the mint authority—even a renounced, fixed-supply token can still shrink.\n\n1. Burn from a holding you own.\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- burn \\\n        --definition-account <DEF> \\\n        --user-holding-account <HOLDING> \\\n        --amount-to-burn <AMOUNT>\n   ```\n\n   - Burning more than the holding's balance is rejected.\n\n## Step 8: NFTs\n\nA non-fungible token is a definition with a `printable_supply` plus a metadata account. Its first holding is an `NftMaster`; each print carves an `NftPrintedCopy` out of it. `print_balance` reserves one unit for the master itself, so a `printable_supply` of `N` yields `N − 1` printable copies.\n\n> **Warning**\n>\n> `spel` cannot currently create NFT (or metadata-bearing fungible) definitions—`new-definition-with-metadata` takes two structured arguments that `spel` has no way to encode (it can only encode enum-kinded IDL types, and neither argument's type appears in the `types` section of the IDL). This still holds as of CLI v0.7.0. Until it's supported, create these definitions programmatically; see `token_program::new_definition::new_definition_with_metadata` and the integration tests in `programs/integration_tests/tests/token.rs` in the [`lez-programs`](https://github.com/logos-blockchain/lez-programs) repository. Everything below works over the CLI against a definition created that way.\n\n1. Print a copy. Both the master holding and the fresh printed-copy account must sign; the printed target must be fresh—`print-nft` claims it itself, so don't pre-initialise it.\n\n   ```sh\n   wallet account new public --label \"NFT copy #1\"\n   ```\n\n   ```sh\n   spel --idl artifacts/token-idl.json \\\n        --program <TOKEN_PROGRAM_ID> \\\n        -- print-nft \\\n        --master-account <NFT_MASTER_HOLDING> \\\n        --printed-account <NEW_COPY_HOLDING>\n   ```\n\n   - Each print decrements the master's `print_balance` by 1. Printing requires `print_balance > 1`, so the last unit can never be printed.\n\n1. Move a printed copy. Initialise a slot for the recipient against the NFT definition ([Step 3](#step-3-give-someone-a-holding))—it produces an un-owned `NftPrintedCopy`—then transfer `1` ([Step 4](#step-4-transfer)).\n\n   ```sh\n   spel --idl artifacts/token-idl.json --program <TOKEN_PROGRAM_ID> \\\n        -- initialize-account --definition-account <NFT_DEF> --account-to-initialize <RECIPIENT_SLOT>\n\n   spel --idl artifacts/token-idl.json --program <TOKEN_PROGRAM_ID> \\\n        -- transfer --sender <MY_COPY> --recipient <RECIPIENT_SLOT> --amount-to-transfer 1\n   ```\n\n1. Transfer a master. This is only meaningful into an existing `NftMaster` holding sitting at `print_balance: 0`, with the transferred amount equal to the sender's whole `print_balance`. `initialize-account` always produces a printed-copy slot, never a master, so in practice a master can only move to an account that previously gave one away—plan master ownership at creation time.\n\n## Step 9: Inspect a token account\n\nAny token account can be read back at any time, with no signing and no transaction submitted.\n\n```sh\nspel --idl artifacts/token-idl.json inspect <DEF>      --type TokenDefinition\nspel --idl artifacts/token-idl.json inspect <HOLDING>  --type TokenHolding\nspel --idl artifacts/token-idl.json inspect <METADATA> --type TokenMetadata\n```\n\n`--type` must match the account's actual shape—decoding a holding as a definition fails. A holding's `definition_id` field is how you find the token it belongs to.\n\n## Troubleshooting\n\n### `Transaction NOT confirmed` on a transaction that actually landed\n\n`spel` stops waiting after a fixed number of blocks and prints `❌ Transaction NOT confirmed: Transaction not found in preconfigured amount of blocks`. It prints that same line whether the transaction was rejected by the program or simply confirmed too slowly. Check the affected accounts with `inspect` ([Step 9](#step-9-inspect-a-token-account)) before re-running—re-running a `transfer` that did land moves the balance a second time.\n\n### `Token definition must be owned by token program`\n\nThe token definition was created by a different build of the token program than the one you're calling. Recompiling the token program changes its ProgramId, and tokens created by an older build stay owned by it—recreate the token against the build you're currently using.\n\n### A transfer or mint rejects a fresh recipient account\n\nNeither `transfer` nor `mint` marks its recipient/holder as a signer, so the claim of a fresh account can't be authorised. Run `initialize-account` on the recipient first ([Step 3](#step-3-give-someone-a-holding)). `print-nft` is the one exception—its printed target *is* a signer, so it must be fresh and must not be pre-initialised.\n\n### `Option` or account arguments are rejected as missing\n\n`spel` has no optional arguments. `--mint-authority` and `--new-authority` must always be passed—use the literal `none` for a fixed-supply token or a renounced authority. Similarly, there's no optional-account mechanism, which is why authority operations are split into `mint`/`mint-with-authority` and `set-authority`/`set-authority-with-authority`; using the variant that doesn't match where the authority lives fails with `signer is not the current authority`, even when you hold the right key.\n\n### `--dry-run` submits the transaction anyway\n\n`--dry-run` is a global flag and only works **before** the `--` separator: `spel --idl … --program … --dry-run -- transfer …` resolves and prints the transaction without submitting it. Placed after the instruction name, it's silently ignored and the transaction **is** submitted.\n\n### `spel` rejects an account id argument\n\nAccount ids must be passed as bare base58 or `0x`-prefixed hex. Strip the wallet's `account_id(...)` display wrapper before passing an id to `spel`.\n"},{"id":"lez/transfer-tokens/transfer-lez-tokens-between-public-private-states","title":"Transfer LEZ tokens between public and private states","summary":"Get started with private transfers to accounts you don't control, using a recipient-published keypair.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/transfer-tokens/transfer-lez-tokens-between-public-private-states","markdown_url":"https://docs.logos.co/lez/transfer-tokens/transfer-lez-tokens-between-public-private-states.md","permalink":"/lez/transfer-tokens/transfer-lez-tokens-between-public-private-states","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/transfer-tokens/transfer-lez-tokens-between-public-private-states.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Publish a reusable keypair as the recipient"},{"depth":2,"text":"Step 2: Send funds to the recipient's private account"},{"depth":2,"text":"Step 3: Discover and spend the incoming funds as the recipient"},{"depth":2,"text":"Troubleshooting private foreign transfers"},{"depth":3,"text":"Why does the recipient not see the funds after `sync-private`?"},{"depth":3,"text":"Why does the second transfer to the same (NPK, identifier) fail?"},{"depth":3,"text":"Why does spending multiple incoming transfers become expensive?"}],"markdown":"# Transfer LEZ tokens between public and private states\n\n#### Get started with private transfers to accounts you don't control, using a recipient-published keypair.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure covers how to credit a [private account](https://docs.logos.co/get-started/glossary.md#private-account)—regular or a [Program Derived Address](https://docs.logos.co/get-started/glossary.md#program-derived-address) ([PDA](https://docs.logos.co/get-started/glossary.md#pda))—that you do not control, using only the recipient's published keypair and the sender's chosen identifier. It is intended for wallet users on testnet v0.3 who need to make private payments without interactive setup or per-sender [account](https://docs.logos.co/get-started/glossary.md#account) registration. For example, a recipient can publish one keypair and receive from many independent senders, each into a separate account.\n\n> **Info: Prerequisites**\n>\n>\n> - An [LEZ CLI wallet](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md) set up and funded.\n\n## What to expect\n\n- You can credit a recipient's private account using only their published [NPK](https://docs.logos.co/get-started/glossary.md#npk) and [VPK](https://docs.logos.co/get-started/glossary.md#vpk), with no interactive setup required.\n- You can receive tokens into up to 2^128 distinct accounts from the same keypair by varying the identifier, so one published key serves many independent senders.\n- You can discover and spend incoming funds with `wallet account sync-private` after the sender's transaction is confirmed.\n- Transfers work for both native LEZ tokens and [custom tokens](https://docs.logos.co/lez/transfer-tokens/create-and-transfer-custom-tokens-on-lez.md).\n\n## Step 1: Publish a reusable keypair as the recipient\n\nThe recipient generates a reusable keypair (NPK and VPK) and shares it with senders.\n\n1. Generate and publish a reusable keypair:\n\n   ```sh\n   wallet account new private-accounts-key\n   ```\n\n   - This prints the `chain_index`, NPK, and VPK. No account is bound to the keypair yet.\n\n1. Export the keys to share with senders:\n\n   ```sh\n   wallet account show-keys --account-id <recipient-account-or-label>\n   ```\n\n   - Pass the `Private/...` ID that `wallet account ls` lists at the path printed in the previous step (for example, `/0`).\n   - This prints NPK (hex) then VPK (hex). Save the output to a file to use with `--to-keys` later.\n\n## Step 2: Send funds to the recipient's private account\n\nThe sender credits the recipient's account at a chosen identifier. The circuit initialises the account, emits its commitment and a deterministic nullifier, and encrypts the post-state to the recipient by ML-KEM-768 encapsulation against the recipient VPK.\n\n> **Warning**\n>\n> Two senders who independently pick the same identifier for the same NPK target the same account. The second transfer fails at the commitment/nullifier layer. Use high-entropy identifiers to avoid collisions.\n\n1. Send funds to the recipient's private account using their published keys:\n\n   ```sh\n   wallet auth-transfer send \\\n     --from <sender-account-or-label> \\\n     --to-npk <recipient-npk-hex> \\\n     --to-vpk <recipient-vpk-hex> \\\n     --to-identifier <u128> \\\n     --amount 100\n   ```\n\n   - Pass `--to-keys <file>` instead of `--to-npk`/`--to-vpk` if you saved the keys to a file in Step 1.\n\n## Step 3: Discover and spend the incoming funds as the recipient\n\nThe recipient scans for incoming transfers, decrypting the `PrivateAccountKind` header to reconstruct the account ID, then spends from the discovered account.\n\n1. Scan and decrypt incoming funds:\n\n   ```sh\n   wallet account sync-private\n   ```\n\n   - The wallet decrypts the ciphertext header for each incoming transfer and reconstructs `AccountId = SHA256(prefix || npk || vpk || identifier)` for accounts where the VPK matches.\n   - Each incoming transfer appears as a separate account (`PrivateOwned` or `PrivatePdaOwned`), distinguished by identifier.\n\n1. Spend from the discovered account:\n\n   ```sh\n   wallet auth-transfer send \\\n     --from <recipient-account> \\\n     --to-npk <next-npk> \\\n     --to-vpk <next-vpk> \\\n     --to-identifier <u128> \\\n     --amount 50\n   ```\n\n   - The recipient account must have been discovered by `sync-private` in the previous step before it can be used as a `--from` source.\n\n## Troubleshooting private foreign transfers\n\n### Why does the recipient not see the funds after `sync-private`?\n\nThe wallet reconstructs the account ID from the decrypted `PrivateAccountKind` header using the exact NPK, VPK, and identifier the sender used. Confirm that the sender used the NPK and VPK published by the recipient in Step 1 and the agreed-upon identifier, then re-run `wallet account sync-private`.\n\n### Why does the second transfer to the same (NPK, identifier) fail?\n\nTwo transfers to the same `(npk, identifier)` pair resolve to the same `AccountId` and collide at the commitment/nullifier layer (`check_commitments_are_new` / `check_nullifiers_are_valid` in `lee/state_machine/src/state/mod.rs`). Each sender must use a unique identifier for a given recipient NPK.\n\n### Why does spending multiple incoming transfers become expensive?\n\nThere is no automatic balance consolidation. N transfers to the same keypair produce N separate accounts, and spending all of them requires N inputs, so proof and transaction cost grows linearly with N. Plan identifier allocation to minimise the number of accounts that need to be spent together.\n"},{"id":"lez/follow-chain/run-lez-indexer","title":"Run an LEZ Indexer","summary":"Get started with the LEZ Indexer and query finalised LEZ state over HTTP RPC.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/follow-chain/run-lez-indexer","markdown_url":"https://docs.logos.co/lez/follow-chain/run-lez-indexer.md","permalink":"/lez/follow-chain/run-lez-indexer","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/follow-chain/run-lez-indexer.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Start the LEZ stack and run the indexer"},{"depth":2,"text":"Verify the indexer is progressing"},{"depth":2,"text":"Query account state using the wallet and RPC"},{"depth":2,"text":"Frequently asked questions"},{"depth":3,"text":"Why is there a delay between sending a transaction and seeing it in the indexer?"},{"depth":3,"text":"How do I change the indexer configuration?"}],"markdown":"# Run an LEZ Indexer\n\n#### Get started with the LEZ Indexer and query finalised LEZ state over HTTP RPC.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThe [LEZ](https://docs.logos.co/get-started/glossary.md#lez) Indexer is a service that reads the finalised LEZ state from the [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) node and exposes it over an HTTP RPC interface. Any application can use this interface to query blocks, transactions, and [account](https://docs.logos.co/get-started/glossary.md#account) state. This procedure walks you through running an LEZ Indexer locally alongside its dependencies, verifying it is indexing blocks, and querying account state through the RPC.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux\n>    - macOS\n> - A local clone of the [LEZ repository](https://github.com/logos-blockchain/logos-execution-zone).\n> - An [LEZ CLI wallet](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md) set up and funded.\n> - [`just`](https://github.com/casey/just) installed.\n> - [Docker](https://docs.docker.com/get-docker/) installed.\n> - A running [Logos Blockchain node](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md) (if not deploying locally).\n> - Port `8779` free on your machine.\n\n## What to expect\n\n- You can query finalised LEZ blockchain state including blocks, transactions, and accounts via HTTP RPC on `http://localhost:8779`.\n- You have a running local stack with the Logos Blockchain node, LEZ Sequencer, and LEZ Indexer coordinated through `just` commands.\n- You can verify indexer health and confirm it is progressing by calling the `getLastFinalizedBlockId` and `checkHealth` RPC methods.\n\n## Start the LEZ stack and run the indexer\n\nThe indexer depends on the Logos Blockchain Node and benefits from an LEZ Sequencer running alongside it. For a local deployment, start each service in its own terminal.\n\n1. In the LEZ repository, run `just clean` to remove any leftover artifacts from previous runs.\n1.  Replace the default value of `bedrock_config.addr` in `lez/indexer/service/configs/debug/indexer_config.json` with your Logos Blockchain endpoint and replace the default `channel_id` there with the LEZ's [channel](https://docs.logos.co/get-started/glossary.md#channel) ID. Get the channel ID from the public LEZ sequencer by running:\n\n    ```bash\n    curl https://testnet.lez.logos.co/ \\\n     -H \"Content-Type: application/json\" \\\n     -d \"{ \\\n         \\\"jsonrpc\\\": \\\"2.0\\\", \\\n         \\\"method\\\": \\\"getChannelId\\\", \\\n         \\\"params\\\": {}, \\\n         \\\"id\\\": 1 \\\n     }\"\n    ```\n\n    For a local deployment, run the following commands in two separate terminal windows:\n\n    ```sh\n    # Run node, in terminal 1\n    docker compose up logos-blockchain-node-0\n    ```\n\n    ```sh\n    # Terminal 2\n    just run-sequencer\n    ```\n\n    - Test configs are located [here for the node](https://github.com/logos-blockchain/logos-execution-zone/tree/main/bedrock) and [here for the sequencer](https://github.com/logos-blockchain/logos-execution-zone/blob/main/lez/sequencer/service/configs/debug/sequencer_config.json).\n1.  In a third terminal window, start the LEZ Indexer:\n\n    ```sh\n    just run-indexer\n    ```\n\n    The indexer serves JSON-RPC over HTTP (and WebSocket) on `http://localhost:8779` by default. At startup, you will see:\n\n    ```\n    [2026-06-18T14:21:59Z INFO  indexer_service] Starting Indexer Service RPC server on 0.0.0.0:8779\n    [2026-06-18T14:21:59Z INFO  indexer_core] Starting indexer from beginning of channel\n    ```\n\n    The indexer then listens for block finalisations.\n\n## Verify the indexer is progressing\n\n1.  Call `getLastFinalizedBlockId` to confirm the indexer is receiving blocks:\n\n    ```sh\n    curl -X POST http://localhost:8779 \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"jsonrpc\":\"2.0\",\"method\":\"getLastFinalizedBlockId\",\"params\":[],\"id\":1}'\n    ```\n\n    You will receive a response similar to:\n\n    ```sh\n    {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":15}\n    ```\n\n    The `result` value should increase each time you repeat the call.\n1.  Run the health check to confirm the indexer can reconstruct the full state from the database:\n\n    ```sh\n    curl -X POST http://localhost:8779 \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"jsonrpc\":\"2.0\",\"method\":\"checkHealth\",\"params\":[],\"id\":1}'\n    ```\n\n    A healthy indexer returns:\n\n    ```sh\n    {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}\n    ```\n\n    > **Info**\n    >\n    > `checkHealth` reconstructs the full LEZ state from the database and can produce heavy disk usage. Run it sparingly.\n\n## Query account state using the wallet and RPC\n\n1.  In a fourth terminal window, query an account state by calling `getAccount` with the account ID:\n\n    ```sh\n    curl -X POST http://localhost:8779 \\\n      -H \"Content-Type: application/json\" \\\n      -d '{\"jsonrpc\":\"2.0\",\"method\":\"getAccount\",\"params\":{\"account_id\":\"2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2\"},\"id\":1}'\n    ```\n\n    You will receive a response similar to:\n\n    ```sh\n    {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"program_owner\":\"FeYNA4PVvs1SsSuECrmQNnJqtB1jA6CbVBBSVGos7qcu\",\"balance\":20000,\"data\":\"\",\"nonce\":1}}\n    ```\n1. Run `just run-wallet --help` to see all available wallet commands.\n\n## Frequently asked questions\n\n### Why is there a delay between sending a transaction and seeing it in the indexer?\n\nThe delay is caused by Logos Blockchain's block finality time. The indexer only processes finalised blocks from the L1 node, so transactions sent through the sequencer are not visible until the corresponding block is finalised. This is expected behaviour.\n\n### How do I change the indexer configuration?\n\nThe indexer config used by `just run-indexer` is located at [lez/indexer/service/configs/debug/indexer\\_config.json](https://github.com/logos-blockchain/logos-execution-zone/blob/main/lez/indexer/service/configs/debug/indexer_config.json) (a `configs/docker/indexer_config.json` variant is used for the Docker setup). Two fields control connectivity:\n\n- `bedrock_config`—the HTTP address of the Logos Blockchain node.\n- `channel_id`—the channel that LEZ Sequencer writes into. This must match the sequencer you are using.\n"},{"id":"lez/follow-chain/start-and-use-instance-of-lez-explorer","title":"Start and use an instance of the LEZ Explorer","summary":"Get a clear view of on-chain activity including blocks, transactions, and accounts.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/follow-chain/start-and-use-instance-of-lez-explorer","markdown_url":"https://docs.logos.co/lez/follow-chain/start-and-use-instance-of-lez-explorer.md","permalink":"/lez/follow-chain/start-and-use-instance-of-lez-explorer","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/follow-chain/start-and-use-instance-of-lez-explorer.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Start the LEZ Explorer"},{"depth":2,"text":"Browse blocks, transactions, and accounts"},{"depth":2,"text":"Troubleshooting LEZ Explorer"},{"depth":3,"text":"Why does the Explorer show \"No blocks found\" after startup?"},{"depth":3,"text":"Why does LEZ Explorer fail to load block data?"}],"markdown":"# Start and use an instance of the LEZ Explorer\n\n#### Get a clear view of on-chain activity including blocks, transactions, and accounts.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThe [LEZ](https://docs.logos.co/get-started/glossary.md#lez) Explorer lets you inspect the state of the [Logos Execution Zone](https://docs.logos.co/get-started/glossary.md#logos-execution-zone) in real time. This procedure walks you through starting all required services locally and using the Explorer UI to browse blocks, search transactions, and look up [account](https://docs.logos.co/get-started/glossary.md#account) balances. It is intended for developers working on testnet v0.3 who want to verify on-chain state or test wallet interactions.\n\nIf you don't want to run your own LEZ explorer instance, navigate instead to the [public LEZ explorer](https://explorer.testnet.lez.logos.co/).\n\n> **Info: Prerequisites**\n>\n>\n> - [`cargo-leptos`](https://crates.io/crates/cargo-leptos) installed.\n> - The WebAssembly Rust target, which `cargo-leptos` uses to build the Explorer front end: `rustup target add wasm32-unknown-unknown`. Without it, `just run-explorer` fails with ``error[E0463]: can't find crate for `core` ``.\n> - A running instance of an [LEZ Indexer](https://docs.logos.co/lez/follow-chain/run-lez-indexer.md).\n\n## What to expect\n\n- You can browse all committed blocks and click through to inspect individual block details.\n- You can search for transactions and accounts and view their current state and balances.\n\n## Start the LEZ Explorer\n\n1. Navigate to your local clone of the [LEZ repository](https://github.com/logos-blockchain/logos-execution-zone/).\n1.  In a new terminal window, start the LEZ Explorer:\n\n    ```bash\n    just run-explorer\n    ```\n\n    > **Info**\n    >\n    > By default, the LEZ Explorer connects to the LEZ Indexer at `http://localhost:8779`. Set the `INDEXER_RPC_URL` environment variable or pass `--indexer-rpc-url` to use a different address.\n1.  Open `http://localhost:3000/` in your browser.\n\n    You will see a **No blocks found** message initially:\n\n    ![No blocks found screen](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/start-and-use-instance-of-lez-explorer/no-blocks-found.png)\n\n    After approximately one minute, refresh the page. Blocks will appear:\n\n    ![Blocks visible after refresh](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/start-and-use-instance-of-lez-explorer/updated-page.png)\n\n## Browse blocks, transactions, and accounts\n\n1.  Click any block in the list to view its details.\n\n    ![Block detail view](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/start-and-use-instance-of-lez-explorer/block-details.png)\n1.  Use the search bar to look up a block, transaction, or account by ID.\n\n    ![Transaction search result](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/start-and-use-instance-of-lez-explorer/transaction-details.png)\n\n    ![Account detail view](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/start-and-use-instance-of-lez-explorer/account-details.png)\n\n## Troubleshooting LEZ Explorer\n\n### Why does the Explorer show \"No blocks found\" after startup?\n\nBlock production takes up to one minute after the stack starts. Refresh the page manually; live updates are not yet implemented.\n\n### Why does LEZ Explorer fail to load block data?\n\nConfirm that the LEZ Indexer is running and reachable. By default, the Explorer connects to `http://localhost:8779`. Check that `just run-indexer` is active in its terminal and that no firewall rule is blocking port `8779`.\n"},{"id":"lez/accounts/set-up-shared-private-lez-account","title":"Set up a shared private account","summary":"Get started with group-owned private accounts where every member independently derives the same keys.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/accounts/set-up-shared-private-lez-account","markdown_url":"https://docs.logos.co/lez/accounts/set-up-shared-private-lez-account.md","permalink":"/lez/accounts/set-up-shared-private-lez-account","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/accounts/set-up-shared-private-lez-account.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Generate a sealing key pair for each member"},{"depth":2,"text":"Step 2: Create the group and the shared account"},{"depth":2,"text":"Step 3: Invite a new member by sealing the GMS"},{"depth":2,"text":"Step 4: Join the group and derive the shared account"}],"markdown":"# Set up a shared private LEZ account\n\n#### Get started with group-owned private accounts where every member independently derives the same keys.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure covers how to create a shared [private account](https://docs.logos.co/get-started/glossary.md#private-account) (regular or [PDA](https://docs.logos.co/get-started/glossary.md#pda)) on the [LEZ](https://docs.logos.co/get-started/glossary.md#lez) that is jointly controlled by multiple members. From one 32-byte [Group Master Secret](https://docs.logos.co/get-started/glossary.md#group-master-secret) ([GMS](https://docs.logos.co/get-started/glossary.md#gms)), every member independently derives the same [account](https://docs.logos.co/get-started/glossary.md#account) keys ([NSK](https://docs.logos.co/get-started/glossary.md#nsk), [VSK](https://docs.logos.co/get-started/glossary.md#vsk), [NPK](https://docs.logos.co/get-started/glossary.md#npk), [VPK](https://docs.logos.co/get-started/glossary.md#vpk)), so any member can view and spend the shared balance without an interactive key exchange at spend time. It is intended for developers on testnet v0.3 who need multi-party custody of a private balance or a private PDA.\n\nThis feature is 1-of-n at the key layer: any GMS holder can derive every key and spend the account. Threshold gating must be implemented at the [program](https://docs.logos.co/get-started/glossary.md#program) layer. View-only membership is not supported—any GMS holder gets both viewing and spending capability.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux x86_64 or aarch64\n>    - macOS arm64 or x86_64 (requires full Xcode with the Metal toolchain for the Risc0 guest build)\n> - A local clone of the [LEZ repository](https://github.com/logos-blockchain/logos-execution-zone).\n> - An [LEZ CLI wallet](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md) set up and funded.\n\n## What to expect\n\n- You can create a shared private account from a single GMS so that every invited member independently derives the same NPK, VPK, NSK, and VSK without an interactive key exchange at spend time. (Members must agree on the same PDA `--seed`, `--program-id`, and `--identifier` values; see Step 4.)\n- You can admit new members by sealing the GMS to their sealing public key and having them unseal it locally, with no shared secrets transmitted in the clear.\n- You can create a group-owned PDA family where each PDA is distinguished by an identifier derived from the same group keys.\n\n## Step 1: Generate a sealing key pair for each member\n\nEach member who will join a group needs a one-time [sealing key](https://docs.logos.co/get-started/glossary.md#sealing-keys) pair before the owner can invite them. The sealing key uses ML-KEM-768 and is kept separate from account [viewing keys](https://docs.logos.co/get-started/glossary.md#viewing-keys).\n\n1. Each joining member generates their sealing key pair:\n\n   ```sh\n   wallet group new-sealing-key\n   ```\n\n   - This prints the sealing public key. Each member shares the public key with the group owner.\n   - Run this once per wallet. The secret key is stored locally and never shared.\n   - Save the printed public key. A wallet holds exactly one sealing key: running the command again\n     returns `Error: Sealing key already exists`, and there is no command to print the existing\n     public key again.\n\n## Step 2: Create the group and the shared account\n\nThe group owner creates a local group with a fresh random GMS, then derives a shared account from it. Accounts derived from the same group with the same PDA parameters (`--seed`, `--program-id`, `--identifier`) have identical keys for every member who holds the GMS.\n\n1. Create the group with a fresh random GMS:\n\n   ```sh\n   wallet group new test-group\n   ```\n\n   - The group is registered locally and visible in `wallet group list` (alias `ls`).\n\n1. Create the shared account derived from the group. Use the PDA form and record the `--seed`, `--program-id`, and `--identifier` values—the other members need exactly these values in Step 4 to derive the same account:\n\n   ```sh\n   wallet account new private-gms test-group --pda \\\n     --seed <32-byte-hex> --program-id <program-id-hex> --identifier 0 --label shared-acc\n   ```\n\n   `--identifier` diversifies one PDA from another within the same `(program_id, seed)` family; it defaults to a random value if omitted, so set it explicitly for a shared account.\n\n   - The plain form creates a regular private account owned by this wallet alone:\n\n     ```sh\n     wallet account new private-gms test-group --label shared-acc\n     ```\n\n     It derives with a fresh random identifier on every invocation, so other members running the same command land on different accounts. Use it only when you do not need the account to be reproducible by other GMS holders.\n\n## Step 3: Invite a new member by sealing the GMS\n\nThe owner seals the GMS to each new member's sealing public key using ML-KEM-768. The sealed GMS is safe to transmit over any channel.\n\n1. Seal the group's GMS for the new member using their sealing public key:\n\n   ```sh\n   wallet group invite test-group --key <joining-member-sealing-pubkey-hex>\n   ```\n\n   - Share this hex string with the new member over any channel. Repeat this step for each additional member.\n\n## Step 4: Join the group and derive the shared account\n\nThe new member unseals the GMS using their local sealing secret key and derives their instance of the shared account. With the same PDA parameters the owner used, the derived keys are identical to those held by every other member.\n\n1. Unseal the GMS and store it under a local group name:\n\n   ```sh\n   wallet group join my-copy --sealed <sealed-gms-hex-from-owner>\n   ```\n\n   - The GMS is now stored locally under `my-copy`. Both the owner and this member now hold the same GMS.\n\n1. Derive the shared account from the joined group.\n\n   > **Warning**\n   >\n   > For regular (non-PDA) shared accounts, each `wallet account new private-gms` invocation diversifies the derived keys with a random identifier, so running the plain command in two wallets produces two different accounts even from the same GMS. To land on the same account as the other members, use the PDA form with an agreed `--seed`, `--program-id`, and `--identifier`—with identical values, every GMS holder derives identical keys.\n\n   ```sh\n   wallet account new private-gms my-copy --pda \\\n     --seed <32-byte-hex> --program-id <program-id-hex> --identifier 0\n   ```\n\n   - With the same `--seed`, `--program-id`, and `--identifier` values as the owner used, this produces the same account ID, NPK, VPK, NSK, and VSK as the owner's account derived in Step 2.\n"},{"id":"lez/programs/write-and-deploy-lez-program-with-scaffold","title":"Write and deploy an LEZ program with `logos-scaffold`","summary":"Use `logos-scaffold` to create, build, and deploy a guest program on a local Logos Execution Zone sequencer.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold","markdown_url":"https://docs.logos.co/lez/programs/write-and-deploy-lez-program-with-scaffold.md","permalink":"/lez/programs/write-and-deploy-lez-program-with-scaffold","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/programs/write-and-deploy-lez-program-with-scaffold.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install logos-scaffold"},{"depth":2,"text":"Step 2: Create a new project"},{"depth":2,"text":"Step 3: Set up the project"},{"depth":2,"text":"Step 4: Write your guest program"},{"depth":2,"text":"Step 5: Build the project"},{"depth":2,"text":"Step 6: Start a local sequencer"},{"depth":2,"text":"Step 7: Deploy your program"},{"depth":2,"text":"Step 8: Interact with your program"},{"depth":2,"text":"Use `logos-scaffold run` for the inner loop"},{"depth":2,"text":"Deploy to the testnet"},{"depth":2,"text":"Related documentation"}],"markdown":"# Write and deploy an LEZ program with `logos-scaffold`\n\n#### Use `logos-scaffold` to create, build, and deploy a guest program on a local Logos Execution Zone sequencer.\n\n> **Tip: Version**\n>\n> This document is accurate for `logos-scaffold` **0.4.0** built from `master` at commit `d4b15ff`, which pins LEZ **v0.1.2**. It deploys to a local sequencer only; see [Deploy to the testnet](#deploy-to-the-testnet).\n\n[`logos-scaffold`](https://github.com/logos-co/scaffold) is a project scaffold and CLI tool that manages the full lifecycle of a [LEZ](https://docs.logos.co/get-started/glossary.md#lez) guest [program](https://docs.logos.co/get-started/glossary.md#program)—from project creation to deployment. It pins LEZ and SPEL dependencies, builds a project-local sequencer, and handles wallet interactions, so you can focus on writing your program logic.\n\nThis guide walks each stage separately so you can see what the tool does. Once the pieces are familiar, [`logos-scaffold run`](#use-logos-scaffold-run-for-the-inner-loop) chains them into a single command. For what else scaffold manages, including Basecamp modules, see [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md).\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux: x86_64\n>     - macOS\n> - `git`, and Rust 1.81 or newer with `cargo`.\n> - The Unix process helpers `lsof`, `ps`, and `kill`, and `curl`.\n> - The [RISC Zero toolchain](https://dev.risczero.com/api/zkvm/install), installed with `rzup`:\n>\n>     ```bash\n>     curl -L https://risczero.com/install | bash   # installs rzup; restart your shell afterwards\n>     rzup install rust\n>     rzup install cargo-risczero 3.0.5\n>     ```\n>\n>     `rust` builds the guest programs. `cargo-risczero` provides `r0vm`, which the sequencer needs to execute transactions, and it must be the version LEZ `v0.1.2` pins, `3.0.5`: scaffold only looks for that version, under `~/.risc0/extensions/`. Without it, `localnet start` still reports `localnet ready`, with a warning about `RISC0_SERVER_PATH`, and `deploy` then fails. If you installed `rzup` with a custom `RISC0_HOME`, export `RISC0_SERVER_PATH` with the full path to that `r0vm`.\n> - [Docker](https://docs.docker.com/get-docker/), with the daemon running. Scaffold builds guest programs inside the pinned `risczero/risc0-guest-builder` container by default, so that a `program_id` is the same on every machine. Without Docker, opt out as described in [Step 5](#step-5-build-the-project).\n> - About 5 GB of free disk space for the pinned LEZ and `spel` builds.\n>\n> Nix is not needed for this guide, only for scaffold's `basecamp` commands. If it is not installed, `logos-scaffold doctor` reports a `tool nix` warning that you can ignore.\n\n## What to expect\n\n- You can create an LEZ program project and write a guest program that runs inside the RISC0 zkVM.\n- You can build your program and deploy it to a project-local LEZ sequencer.\n- You can call your deployed program and repeat the whole loop with one command.\n\n## Step 1: Install logos-scaffold\n\n1. Install the CLI from the scaffold repository, pinned to the commit this guide was tested with:\n\n    ```bash\n    cargo install --locked --git https://github.com/logos-co/scaffold --rev d4b15ff logos-scaffold\n    ```\n\n    This installs two binaries on your PATH: `logos-scaffold` and the shorter alias `lgs`. They are functionally identical. The `logos-scaffold` 0.4.0 release on [crates.io](https://crates.io/crates/logos-scaffold) predates the Docker guest build in [Step 5](#step-5-build-the-project), so `cargo install logos-scaffold` gives you a CLI without `--guest` and with guest artifacts in a different place.\n\n1. Verify the installation:\n\n    ```bash\n    logos-scaffold --version\n    ```\n\n## Step 2: Create a new project\n\n1. Create a new LEZ program project. Replace `my-program` with your project name:\n\n    ```bash\n    logos-scaffold new my-program\n    cd my-program\n    ```\n\n    This generates a project with the `default` template, which includes sample guest programs and runner scripts. For an Anchor-style framework with IDL and generated client bindings, pass `--template lez-framework` instead; this guide follows the default template.\n\n    > **Info**\n    >\n    > `new` also clones the pinned LEZ source into scaffold's shared cache; with the builds from [Step 3](#step-3-set-up-the-project) the cache grows to about 3.5 GB. To keep it somewhere else, pass `--cache-root <path>` or set `LOGOS_SCAFFOLD_CACHE_ROOT`.\n\n1. Inspect the project layout:\n\n    ```text\n    my-program/\n    ├── scaffold.toml          # Project configuration and dependency pins\n    ├── methods/\n    │   └── guest/\n    │       └── src/bin/       # Guest programs run inside the RISC0 zkVM\n    ├── src/\n    │   └── bin/               # Runner scripts that submit transactions\n    ├── AGENTS.md              # Guidance for AI coding assistants\n    ├── .claude/skills/        # The same guidance for Claude Code\n    ├── .cursor/rules/         # The same guidance for Cursor\n    └── .scaffold/             # Local state, wallet home, and build artifacts\n    ```\n\n## Step 3: Set up the project\n\n1. Run `setup` to sync the LEZ and SPEL repositories to their pinned commits, build the project-local sequencer and wallet binaries, and seed the default wallet:\n\n    > **Warning**\n    >\n    > Scaffold unlocks the wallet it seeds with a deterministic local password, so the onboarding flow needs no prompts. To use your own, export it **before the first `setup`** (or the first `run`, which chains `setup`):\n    >\n    > ```bash\n    > export LOGOS_SCAFFOLD_WALLET_PASSWORD='<your-local-dev-password>'\n    > ```\n    >\n    > If the storage was already created under the default password, export the override and re-seed with `logos-scaffold run --reset`. These are development-only keys; never use them for real funds.\n\n    ```bash\n    logos-scaffold setup\n    ```\n\n    On a cold cache this step builds the sequencer, wallet, and `spel` from source, which takes about 20 minutes on a four-core machine. Later runs reuse the build.\n\n1. Check the project's environment. `doctor` reports missing tools and configuration problems, with a next step for each:\n\n    ```bash\n    logos-scaffold doctor\n    ```\n\n    At this point expect three `WARN` rows about the sequencer, because the localnet is not running yet. [Step 6](#step-6-start-a-local-sequencer) clears them. A `FAIL` on the `guest build` row means `docker` or `cargo-risczero` is not on your `PATH`; see [Step 5](#step-5-build-the-project).\n\n## Step 4: Write your guest program\n\nGuest programs run inside the [RISC0 zkVM](https://dev.risczero.com/) and define the on-chain logic of your LEZ program. Each guest program in `methods/guest/src/bin/` becomes a deployable program with its own `program_id`.\n\n1. Open the sample guest program:\n\n    ```bash\n    $EDITOR methods/guest/src/bin/hello_world.rs\n    ```\n\n1. Read how the program works. It receives a `ProgramInput` struct via the zkVM environment, applies your logic, and writes a `ProgramOutput` struct to the journal. The sequencer verifies the proof and updates the on-chain [account](https://docs.logos.co/get-started/glossary.md#account) state.\n\n    Key concepts:\n    - **Instructions** are passed as bytes. `hello_world` takes its instruction as a raw `Vec<u8>` and appends it to the account's data.\n    - **Account data** is stored in `AccountWithMetadata` structs.\n    - Use `RISC0_DEV_MODE=1` during development to skip ZK proof generation for faster iteration.\n\n## Step 5: Build the project\n\n1. Build the workspace. In development, use `RISC0_DEV_MODE=1` to skip proof generation:\n\n    ```bash\n    RISC0_DEV_MODE=1 logos-scaffold build\n    ```\n\n    The build compiles the workspace, then builds each guest program inside the pinned `risczero/risc0-guest-builder` Docker image and writes the `.bin` artifacts to `target/riscv-guest-docker/riscv32im-risc0-zkvm-elf/docker/`. The same source then produces the same bytes, and therefore the same `program_id`, on any machine. The first build takes several minutes.\n\n    If the Docker daemon is not running, `build` stops straight away:\n\n    ```text\n    error: [build].guest = \"docker\" needs a running Docker daemon; `docker info` failed.\n    ```\n\n1. To build without Docker, use the host RISC Zero toolchain instead. For one build, pass `--guest local`:\n\n    ```bash\n    RISC0_DEV_MODE=1 logos-scaffold build --guest local\n    ```\n\n    To make it the project default, which [`logos-scaffold run`](#use-logos-scaffold-run-for-the-inner-loop) needs because it has no `--guest` flag, add this to `scaffold.toml`:\n\n    ```toml\n    [build]\n    guest = \"local\"\n    ```\n\n    A local build writes the artifacts to `target/riscv-guest/…/riscv32im-risc0-zkvm-elf/release/` instead. Its `program_id` depends on your host's Rust and clang versions, so it can differ from the one another machine builds from the same source. Each `build` removes the other mode's artifacts, so `deploy` always ships the output of the last `build`. `logos-scaffold doctor` shows the active mode in its `guest build` row.\n\n## Step 6: Start a local sequencer\n\n1. Start a project-local sequencer to deploy your program to:\n\n    ```bash\n    RISC0_DEV_MODE=1 logos-scaffold localnet start\n    ```\n\n    `start` returns once the sequencer process is alive and its RPC port answers. The sequencer is daemonised and survives terminal or tmux session closure. Use `logos-scaffold localnet status` to check that it is running and `logos-scaffold localnet stop` to stop it.\n\n    > **Warning**\n    >\n    > The local sequencer always uses port `3040`. If something else already listens there, such as another scaffold project's sequencer, `localnet start` fails with `Address already in use (os error 98)`, and `localnet status` reports `ownership: foreign`. Stop the other process first. Changing `port` under `[localnet]` in `scaffold.toml` does not help: the sequencer still binds `3040` and the wallet keeps pointing at it, so `deploy` and `wallet topup` would talk to the other sequencer.\n    >\n    > For any other failure, read the sequencer log with `logos-scaffold localnet logs --tail 200`. To wipe chain state and start over, run `logos-scaffold localnet reset --yes`.\n\n## Step 7: Deploy your program\n\n1. Deploy all guest programs to the running sequencer:\n\n    ```bash\n    RISC0_DEV_MODE=1 logos-scaffold deploy\n    ```\n\n    - After each successful submission, `logos-scaffold` prints `program_id: <hex>`, the RISC0 image ID computed locally from the submitted ELF. It prints `program_id: unavailable` if the project's `spel` binary has not been built yet. The example runner scripts in [Step 8](#step-8-interact-with-your-program) load the program from its embedded ELF, so you do not need to copy a `program_id` to complete this guide.\n    - `deploy` checks that the sequencer is reachable before submitting, and stops with a hint if it is not. It submits one program per block, so deploying the five sample programs takes about a minute.\n    - Add `--json` for machine-readable output: one object per program under `{\"deploys\": [...]}`, or a single object with `--program-path … --json`.\n\n    > **Info**\n    >\n    > `deploy` confirms submission, not inclusion. Redeploying a program that is already on the sequencer still reports `submitted`, while the sequencer skips the transaction and logs `ProgramAlreadyExists`, which you can see with `logos-scaffold localnet logs`.\n\n1. To deploy a specific program by name:\n\n    ```bash\n    RISC0_DEV_MODE=1 logos-scaffold deploy hello_world\n    ```\n\n## Step 8: Interact with your program\n\nUse the project-local wallet CLI to submit transactions to your deployed program. The wallet is available at `logos-scaffold wallet`.\n\n1. With the sequencer from Step 6 running, top up the default wallet on your local network, then list your accounts (`wallet list` shows accounts, not a balance):\n\n    ```bash\n    logos-scaffold wallet topup\n    logos-scaffold wallet list\n    ```\n\n    `topup` initialises the account first if it is new, then claims from the Piñata faucet. To confirm the wallet can reach the sequencer, run `logos-scaffold wallet -- check-health`. Everything after `--` goes to the project's wallet binary unchanged.\n\n1. Create a fresh public account for the program to claim. `hello_world` only writes to an account that is uninitialised or already owned by the program, so the topped-up default account does not work:\n\n    ```bash\n    logos-scaffold wallet -- account new public\n    ```\n\n    The output includes `account_id Public/<ID>`. Pass the part after `Public/` to the runner.\n\n1. Run one of the example runner scripts that submit transactions to your program:\n\n    ```bash\n    export NSSA_WALLET_HOME_DIR=\"$(pwd)/.scaffold/wallet\"\n    export LEE_WALLET_HOME_DIR=\"$(pwd)/.scaffold/wallet\"\n    RISC0_DEV_MODE=1 cargo run --bin run_hello_world -- <ID>\n    ```\n\n    The LEZ `v0.1.2` wallet that scaffold pins by default reads `NSSA_WALLET_HOME_DIR`, and LEZ v0.2.0 and later read `LEE_WALLET_HOME_DIR`. Exporting both keeps the runner working on either pin.\n\n    The runner prints the transaction hash. Once the next block is produced, the account holds the greeting `Hola mundo!`, hex-encoded in `data`:\n\n    ```bash\n    logos-scaffold wallet -- account get --account-id Public/<ID>\n    ```\n\n    ```json\n    {\"balance\":0,\"program_owner\":\"...\",\"data\":\"486f6c61206d756e646f21\",\"nonce\":1}\n    ```\n\n    The runner scripts in `src/bin/` demonstrate how to construct and sign a `PublicTransaction`, set the `program_id`, encode an instruction, and submit the transaction via the sequencer RPC.\n\n## Use `logos-scaffold run` for the inner loop\n\nSteps 5 to 8 are often repeated when working on developing an LEZ program. `logos-scaffold run` chains them into one command, and works with no configuration:\n\n```bash\nRISC0_DEV_MODE=1 logos-scaffold run\n```\n\nIt runs these steps in order, printing progress for each:\n\n1. **Build**, which chains `setup` internally.\n1. **Build the IDL**, a no-op for projects that are not built on the `lez-framework` template.\n1. **Start localnet**, or reuse the one already running.\n1. **Top up** the default wallet.\n1. **Deploy**, skipped when the guest binaries, IDL, config, and sequencer are all unchanged since the last `run`. A manual `logos-scaffold deploy` does not count, so the first `run` deploys again.\n1. **Run `post_deploy` hooks**, if the project configures any.\n\nUseful flags:\n\n| Flag | Effect |\n|:---|:---|\n| `--profile NAME` | Select a named pipeline from `[run.profiles.<name>]`. |\n| `--reset` / `--no-reset` | Wipe sequencer state and wallet and re-seed before the run, or override a config-set default. |\n| `--post-deploy <cmd>` | Replace the configured hooks for this run. Repeatable. |\n| `--no-post-deploy` | Skip hooks entirely. |\n| `--watch` | Re-run the pipeline when files change. |\n\nConfigure the loop in `scaffold.toml`:\n\n```toml\n[run]\npost_deploy = [\"cargo run --bin run_hello_world -- <ID>\"]\n\n[run.profiles.demo]\ntopup = false\ndeploy = false\npost_deploy = [\"scripts/demo.sh\"]\n```\n\n- `topup = false` suits a project that funds its own accounts, and `deploy = false` one that deploys from a hook. Both default to `true`.\n\n> **Warning**\n>\n> A named profile is used as-is: selecting one shadows the inline `[run]` values instead of inheriting them. A key the profile does not state falls back to its own default, not to your `[run]` value. Set every key you need in each profile.\n\nHooks run through `sh -c` from the project root with these variables set:\n\n| Variable | Value |\n|:---|:---|\n| `SEQUENCER_URL` | The localnet RPC URL. |\n| `NSSA_WALLET_HOME_DIR`, `LEE_WALLET_HOME_DIR` | The project wallet directory, under both the name LEZ `v0.1.2` reads and the one v0.2.0 and later read. |\n| `SCAFFOLD_PROJECT_ROOT`, `SCAFFOLD_IDL_DIR` | Absolute paths to the project root and the IDL output directory. |\n| `SCAFFOLD_TOPUP_SKIPPED`, `SCAFFOLD_DEPLOY_SKIPPED` | `1` or `0`. Always set, so branch on the value rather than on whether the variable exists. |\n| `SCAFFOLD_PROGRAMS` | Space-separated names of the project's deployable programs. |\n| `SCAFFOLD_PROGRAM_ID`, `SCAFFOLD_GUEST_BIN` | The deployed program's image ID and guest binary. Set only when the project has exactly one deployable program, so a multi-program project fails loudly instead of picking the wrong one. |\n\n`run` covers the deploy loop only. It does not run any `basecamp` command; for that side of scaffold see [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md).\n\n## Deploy to the testnet\n\n> **Warning**\n>\n> `logos-scaffold` cannot deploy to the current LEZ public testnet. Use the local sequencer from this guide.\n\nThe public testnet runs LEZ v0.3, and scaffold pins `v0.1.2` by default. Pointing the project wallet at `https://testnet.lez.logos.co/` fails the compatibility check: `logos-scaffold wallet -- check-health` panics with `Missing authenticated transfer ID from remote`, and `logos-scaffold doctor` reports `FAIL | wallet usability`.\n\nTo deploy to the testnet today, set up a standalone wallet as described in [Run an LEZ wallet via the CLI](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md), built from the LEZ release the testnet runs, and use its `deploy-program` command with a guest program built against that same release. Check the wallet with `wallet check-health` first: at the time of writing, a wallet built from LEZ `v0.3.1` passes against the testnet and one built from scaffold's `v0.1.2` pin does not.\n\n## Related documentation\n\n- [About Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/about-logos-scaffold.md)\n- [Develop a Logos module with Logos Scaffold](https://docs.logos.co/build-an-app/scaffold/get-started/develop-a-logos-module-with-logos-scaffold.md)\n- [Troubleshoot Logos module development with Basecamp](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md)\n- [Run the LEZ wallet via CLI](https://docs.logos.co/lez/get-started/run-lez-wallet-via-cli.md)\n- [Scaffold command reference](https://github.com/logos-co/scaffold/blob/master/docs/commands.md) and [`scaffold.toml` run configuration](https://github.com/logos-co/scaffold/blob/master/docs/configuration.md) in the scaffold repository\n"},{"id":"lez/rln/register-rln-membership-from-basecamp","title":"Register an RLN membership from Basecamp","summary":"Get an RLN membership on the LEZ testnet for a delivery node running in Logos Basecamp.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/rln/register-rln-membership-from-basecamp","markdown_url":"https://docs.logos.co/lez/rln/register-rln-membership-from-basecamp.md","permalink":"/lez/rln/register-rln-membership-from-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/rln/register-rln-membership-from-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install the Delivery Demo app"},{"depth":2,"text":"Step 2: Create the delivery node"},{"depth":2,"text":"Step 3: Find the payer account"},{"depth":2,"text":"Step 4: Fund the payer account"},{"depth":2,"text":"Step 5: Wait for the membership to register"},{"depth":2,"text":"Troubleshooting RLN membership in Basecamp"},{"depth":3,"text":"The RLN Membership app shows \"Setup failed\" with `bridge_failure: Call refused`"},{"depth":3,"text":"Membership stays `unknown`"},{"depth":3,"text":"The balance check returns `0` long after a bridge deposit"}],"markdown":"# Register an RLN membership from Basecamp\n\n#### Get an RLN membership on the LEZ testnet for a delivery node running in Logos Basecamp.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.1**.\n\nThis procedure covers how to get an RLN membership on the [LEZ](https://docs.logos.co/get-started/glossary.md#lez) testnet registry for a Logos Delivery node that runs in Logos Basecamp. On the `logos.test` network, a node needs an active membership before any message it sends goes out. You create the node from the Delivery Demo [app](https://docs.logos.co/get-started/glossary.md#app), fund the payer account that the node derives, and the node registers its membership by itself. Use this procedure before you send messages on `logos.test` from Basecamp, for example with the Logos Chat app.\n\nRegistration pays with native tokens from the payer account. There is no faucet: you fund the payer yourself, for example by bridging tokens from the Logos Blockchain.\n\n> **Info: Prerequisites**\n>\n>\n> - [Logos Basecamp installed](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - Network access to the testnet sequencer [`https://testnet.lez.logos.co/`](https://testnet.lez.logos.co/).\n> - A way to send `250000000` native units to an account on the LEZ testnet, such as Logos Blockchain notes that you can [bridge to the zone](https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md).\n> - Approximately 1 GB free disk for the downloaded module packages.\n\n## What to expect\n\n- You can create a `logos.test` delivery node in Basecamp and find the payer account it registers from.\n- You can fund the payer and watch the node register its own membership, with no wizard and no faucet.\n- You can confirm the membership's state and message quota in the **RLN** panel of Delivery Demo.\n\n## Step 1: Install the Delivery Demo app\n\n1. In Basecamp's **Package Manager**, find **Delivery Demo** in the official catalogue and click **INSTALL**.\n1. In the **Install Package?** dialogue, click **Install**.\n\n   - The dialogue lists `delivery_module` 0.4.0, `liblogos_lez_rln_module` 5.1.0, and `liblogos_rln_module` 0.12.0. These modules hold the node's wallet and register its membership. No extra package repository is needed.\n\n   **Expected result:** the **Delivery Demo** row reads **INSTALLED**. If the row shows **RETRY**, click it once more.\n\n## Step 2: Create the delivery node\n\n1. Open **Delivery Demo** from the **Applications** grid.\n1. In the `createNode` panel, keep `preset` set to `logos.test` and `mode` set to `Core`, then click **Call**.\n\n   **Expected result:** the status badges read `RLN READY` and `CONNECTED`. The **RLN** panel shows **Membership** `unknown` and **Messages left this epoch** `0 / 0`, because the node has no membership yet.\n\n   ![Delivery Demo after creating the node, with the RLN panel showing Membership unknown and 0 / 0 messages left](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/register-rln-membership-from-basecamp/delivery-demo-membership-unknown.png)\n\n## Step 3: Find the payer account\n\nCreating the node also creates its LEZ wallet and derives a payer account from it. The payer pays for the registration.\n\n1. Read the payer account ID from the wallet in Basecamp's data directory. On Linux, with the default data directory:\n\n   ```bash\n   cat ~/.local/share/Logos/LogosBasecamp/module_data/liblogos_lez_rln_module/*/wallet-home/payer.json\n   ```\n\n   **Expected result:** a 64-character hex account ID.\n\n   - If you started Basecamp with `--user-dir <path>`, replace `~/.local/share/Logos/LogosBasecamp` with that path.\n   - Basecamp's log also names the payer, in a line that reads `derived the payer <ACCOUNT> — it holds nothing until something transfers to it`.\n\n1. Convert the account ID to the base58 form that the sequencer and LEZ wallets use. Replace `<ACCOUNT>` with the hex value:\n\n   ```bash\n   ACCOUNT=<ACCOUNT>\n   python3 -c '\n   import sys\n   A = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\"\n   h = sys.argv[1]\n   n, s = int(h, 16), \"\"\n   while n:\n       n, r = divmod(n, 58)\n       s = A[r] + s\n   print(\"1\" * ((len(h) - len(h.lstrip(\"0\"))) // 2) + s)' $ACCOUNT\n   ```\n\n## Step 4: Fund the payer account\n\nTo register, the payer must hold the registry's price plus a fee reserve: `182800000` native units in total at the testnet's minimum fee. Sending `250000000` leaves headroom. A registration spends about `75000000`, and the rest stays in the payer account.\n\n1. Send `250000000` native units to the payer account.\n\n   - To bridge from the Logos Blockchain, follow [Bridge assets between Logos Blockchain and a Zone](https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md), with the payer account ID as the deposit's recipient. A deposit is credited on LEZ only once it is final on the blockchain, which takes about 10 hours on the current testnet.\n\n1. Check the payer's balance. Replace `<BASE58>` with the value from Step 3:\n\n   ```bash\n   curl -sS -X POST -H 'Content-Type: application/json' \\\n     --data '{\"jsonrpc\":\"2.0\",\"method\":\"getAccountBalance\",\"params\":[\"<BASE58>\"],\"id\":1}' \\\n     https://testnet.lez.logos.co/\n   ```\n\n   **Expected result:** `result` is at or above `182800000`.\n\n## Step 5: Wait for the membership to register\n\nThe node registers its membership as soon as it sees a funded payer.\n\n1. Keep Basecamp running with the node from Step 2.\n\n   - If you closed Basecamp after Step 2, start it again, open **Delivery Demo**, and click **Call** in the `createNode` panel with the same values. The node finds the funded payer and registers.\n\n1. Watch the **RLN** panel in Delivery Demo.\n\n   **Expected result:** within a few minutes of the payer being funded, **Membership** reads `active`, **Messages left this epoch** reads `100 / 100`, and a **Membership** hash appears below.\n\n   ![The RLN panel of Delivery Demo showing Membership active, 100 / 100 messages left, and the membership hash](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/lez/assets/register-rln-membership-from-basecamp/delivery-demo-membership-active.png)\n\nThe membership is stored with the node's wallet in Basecamp's data directory. Later launches of Basecamp pick it up, and nothing is registered or paid again.\n\n## Troubleshooting RLN membership in Basecamp\n\n### The RLN Membership app shows \"Setup failed\" with `bridge_failure: Call refused`\n\nThe **RLN Membership** app (`rln_membership_ui` 0.7.4) from the **Logos RLN Membership** package repository registers through a faucet that the current RLN modules no longer have. It can't complete on the current testnet. Register through a delivery node as described on this page instead. You can remove the **Logos RLN Membership** repository under **Manage Repositories**.\n\n### Membership stays `unknown`\n\nThe payer holds less than `182800000`. Check its balance as in [Step 4](#step-4-fund-the-payer-account). A bridge deposit shows up on LEZ only about 10 hours after it is included on the blockchain.\n\n### The balance check returns `0` long after a bridge deposit\n\nThe deposit isn't final on the blockchain yet. Wait, and check that the deposit's recipient was the payer account ID from [Step 3](#step-3-find-the-payer-account).\n"},{"id":"lez/rln/register-rln-membership-on-lez-with-delivery-node","title":"Register an RLN membership on LEZ with a Delivery node","summary":"Start a Logos Delivery node on the DoS-protected logos.test network, register its RLN membership on the Logos Execution Zone, and send a rate-limited message.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/rln/register-rln-membership-on-lez-with-delivery-node","markdown_url":"https://docs.logos.co/lez/rln/register-rln-membership-on-lez-with-delivery-node.md","permalink":"/lez/rln/register-rln-membership-on-lez-with-delivery-node","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/rln/register-rln-membership-on-lez-with-delivery-node.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Set up a working directory"},{"depth":2,"text":"Step 2: Start the daemon with a wallet home"},{"depth":2,"text":"Step 3: Install and load the modules"},{"depth":2,"text":"Step 4: Create the delivery node"},{"depth":2,"text":"Step 5: Fund the node's payer account"},{"depth":2,"text":"Step 6: Wait for the membership to register"},{"depth":2,"text":"Step 7: Start the node and send a message"},{"depth":2,"text":"Troubleshooting RLN membership"},{"depth":3,"text":"A string argument arrives as a number"},{"depth":3,"text":"`createNode` fails with an unknown preset"},{"depth":3,"text":"`rlnState` stays `Disabled`"},{"depth":3,"text":"`rlnState` reports `Failed`"},{"depth":3,"text":"`wallet_status` reports `failed`, or `daemon.log` shows `Unexpected variant tag`"},{"depth":3,"text":"`wallet_status` stays `pending` with the network selected"},{"depth":3,"text":"`wallet_status` stays `pending` with `no network selected`"},{"depth":3,"text":"The wallet never becomes ready, or opens in the wrong place"},{"depth":3,"text":"Registry calls fail with `bound to network`"},{"depth":3,"text":"`provisioning.step` stays at `awaiting_funding`"},{"depth":3,"text":"Registration fails with `Incorrect fee`"},{"depth":3,"text":"Registration never finishes and `daemon.log` contains `outside registry bounds`"},{"depth":3,"text":"`get_epoch_quota` returns `rate_limit: 0`"},{"depth":3,"text":"`send` fails with `no_usable_membership`"},{"depth":3,"text":"Receivers drop the first message after registration"},{"depth":3,"text":"`daemon start` fails, or `daemon status` reports `not_running`"},{"depth":3,"text":"Every command fails with `no daemon`"},{"depth":3,"text":"`package install` does nothing and asks for confirmation"},{"depth":3,"text":"`package install` can't find `liblogos_rln_module`"},{"depth":3,"text":"After a restart the membership reads `unknown` and sends fail"}],"markdown":"# Register an RLN membership on LEZ with a Delivery node\n\n#### Start a Logos Delivery node on the DoS-protected logos.test network, register its RLN membership on the Logos Execution Zone, and send a rate-limited message.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure shows how to register an Rate-Limiting Nullifier (RLN) membership on the [Logos Execution Zone](https://docs.logos.co/get-started/glossary.md#logos-execution-zone), used to protect against denial-of-service attacks. Messages published on the LEZ must include a proof of membership. It is intended for node operators who want to join the DoS-protected network, and touches Logos Core, [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery), and LEZ in a single flow.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - macOS (Apple Silicon)\n>    - Linux (x86-64 or aarch64)\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n> - `curl` and `python3`\n> - Network access to `github.com` for the catalogue downloads, and to the testnet sequencer at `https://testnet.lez.logos.co/`\n> - About 1 GB of free disk space. The [modules](https://docs.logos.co/get-started/glossary.md#module) take about 300 MB.\n> - `250000000` units of native LEZ tokens for the node's payer [account](https://docs.logos.co/get-started/glossary.md#account), from one of these sources:\n>    - A [Logos Blockchain](https://docs.logos.co/get-started/glossary.md#logos-blockchain) wallet you can [bridge assets](https://docs.logos.co/blockchain/transfer-tokens/bridge-assets-from-logos-blockchain-to-zone-on-basecamp.md) from. Testnet has no faucet.\n>    - A LEZ account you already hold on the testnet zone, with about `270000000` units so it can cover the transfer's fee reserve.\n>\n\n## What to expect\n\n- You can run a `delivery_module` node on `logos.test` with an active RLN membership registered on LEZ.\n- You can fund the node's payer account and watch the node register its membership by itself.\n- You can send a message that carries an RLN proof and confirm that it propagated and used up part of your epoch quota.\n\n## Step 1: Set up a working directory\n\nRun every command in this procedure in one shell, from one working directory.\n\n1. Create a working directory and change into it:\n\n   ```bash\n   mkdir -p ~/logos-rln && cd ~/logos-rln\n   ```\n\n1. Set the session directory. Every `logosctl` command in this shell uses it to hold the daemon's state, its logs, and the installed modules:\n\n   ```bash\n   export LOGOSCTL_CONFIG_DIR=$PWD/session\n   ```\n\n## Step 2: Start the daemon with a wallet home\n\nThe node keeps its wallet, including its private key, in a wallet home directory. There is nothing to configure: the `logos.test` preset names the registry as `logos:testnet:<config account>`, and the LEZ RLN module maps `testnet` to the testnet sequencer from its built-in network table. The directory only fixes where the wallet lives.\n\n1. Create an empty wallet home:\n\n   ```bash\n   mkdir -p wallet-home\n   ```\n\n1. Start the daemon. It passes its environment to the modules, which is how the wallet finds its home:\n\n   ```bash\n   LEE_WALLET_HOME_DIR=$PWD/wallet-home \\\n   NSSA_WALLET_HOME_DIR=$PWD/wallet-home \\\n     logosctl daemon start --detach\n   ```\n\n   **Expected result:** `Daemon started (pid N)` and the path of its log, `session/logs/daemon.log`. The command returns once the daemon is ready.\n\n1. Check the daemon's status:\n\n   ```bash\n   logosctl daemon status --json\n   ```\n\n   **Expected result:** `\"daemon\":{\"status\":\"running\",...}`, with `capability_module`, `package_manager`, and `package_downloader` loaded.\n\n## Step 3: Install and load the modules\n\n1. Install the [delivery module](https://docs.logos.co/get-started/glossary.md#delivery-module) and the RLN module from the official [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue). `liblogos_rln_module` pulls in `liblogos_lez_rln_module`:\n\n   ```bash\n   logosctl package install delivery_module liblogos_rln_module -y\n   ```\n\n1. List the installed [packages](https://docs.logos.co/get-started/glossary.md#package):\n\n   ```bash\n   logosctl package ls\n   ```\n\n   **Expected result:** `delivery_module` 0.3.3 or later, `liblogos_rln_module` 0.12.0 or later, and `liblogos_lez_rln_module` 5.1.0 or later, each with source `user`.\n\n   - Older delivery modules point `logos.test` at a registry that is no longer used, and older LEZ RLN modules link a LEZ wallet that can't sync the LEZ v0.3.1 testnet zone.\n\n1. Load the delivery module. This also loads the RLN modules, which are its optional dependencies:\n\n   ```bash\n   logosctl module load delivery_module\n   logosctl module ls --loaded\n   ```\n\n   **Expected result:** `liblogos_lez_rln_module`, `liblogos_rln_module`, and `delivery_module` are loaded.\n\n## Step 4: Create the delivery node\n\n1. Capture the node's events in the background. The watcher only sees events emitted after it attaches, so start it first:\n\n   ```bash\n   logosctl --json watch delivery_module >> events.jsonl 2>&1 &\n   ```\n\n1. Write the node config and create the node:\n\n   ```bash\n   cat > node.json <<'EOF'\n   {\"mode\": \"Core\", \"preset\": \"logos.test\"}\n   EOF\n   logosctl --json call delivery_module createNode @$PWD/node.json\n   ```\n\n   | Field | Purpose |\n   |:------|:--------|\n   | `mode` | `Core` runs a full relay node. |\n   | `preset` | `logos.test` brings the network settings (cluster 2, 8 auto-shards, built-in entry nodes) and its RLN settings: the registry on the LEZ testnet, the RLN identifier, and the epoch size. |\n   | `messagingOverrides` | Optional per-layer settings, for example `{\"tcp-port\": 30303}`. Keep extra keys inside it: a bare top-level key switches parsing to the legacy flat shape. |\n\n   **Expected result:** `\"success\":true`. Creating the node also starts the RLN module on the registry that the preset names. The LEZ RLN module writes `wallet-home/wallet_config.json`, pointing at the testnet sequencer, and `wallet-home/network.json`, containing `{\"network\":\"testnet\",\"source\":\"table\"}`. From here on, the node registers itself once its payer account is funded.\n\n1. Poll the RLN state until it reports `\"state\":\"Ready\"`:\n\n   ```bash\n   logosctl --json call delivery_module rlnState\n   ```\n\n   **Expected result:** the reply names the RLN scope the node uses:\n\n   ```text\n   \"registryId\":\"logos:testnet:<64 hex>\", \"rlnIdentifier\":\"<64 hex>\", \"epochSizeSec\":<N>\n   ```\n\n1. Keep both values for the RLN calls in later steps. Replace `<registryId>` and `<rlnIdentifier>` with the values from the reply. Write the identifier to a file so that `logosctl` can't mistake it for a number:\n\n   ```bash\n   REGISTRY=<registryId>\n   printf '%s' '<rlnIdentifier>' > rln-id.arg\n   ```\n\n## Step 5: Fund the node's payer account\n\nThe node derives a payer account from its wallet. To register, it must hold the registry's price plus a fee reserve derived from the chain's base fee: `182800000` in total at the testnet's minimum fee, and more if fees rise. Sending `250000000` native units leaves headroom. The unused part of the reserve is refunded: a registration spends about `75000000`, and the rest stays in the payer account.\n\n1. Poll the wallet status until `state` is `ready`:\n\n   ```bash\n   logosctl --json call liblogos_lez_rln_module wallet_status\n   ```\n\n   **Expected result:** `{\"status\":\"ok\",\"result\":{... \"state\":\"ready\", \"network\":\"testnet\", \"payer\":\"<ACCOUNT>\" ...}}`. While the wallet syncs, `state` is `pending`.\n\n1. Convert the payer account ID to the `Public/<base58>` form that LEZ wallets and explorers use. Replace `<ACCOUNT>` with the 64-character hex value of `payer`, which is also the recipient a bridge deposit carries:\n\n   ```bash\n   ACCOUNT=<ACCOUNT>\n   python3 -c '\n   import sys\n   A = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\"\n   h = sys.argv[1]\n   n, s = int(h, 16), \"\"\n   while n:\n       n, r = divmod(n, 58)\n       s = A[r] + s\n   print(\"Public/\" + \"1\" * ((len(h) - len(h.lstrip(\"0\"))) // 2) + s)' $ACCOUNT\n   ```\n\n1. Send `250000000` native units to the payer account:\n\n   - To bridge from the Logos Blockchain, follow [Bridge assets between the blockchain and a zone](https://docs.logos.co/blockchain/zone-sdk/bridge-assets-between-blockchain-and-zone.md). A deposit is credited on LEZ only once it is final on the blockchain. On the current testnet that is about 10 hours after the deposit is included in a block, and the payer balance stays unchanged until then. The deposit's fee is paid on the blockchain side.\n   - To transfer from an existing LEZ account, send to the `Public/<base58>` address. A transfer carries a fee reserve of about `19000000` on top of the amount, at the minimum fee for the wallet's standard 2M gas limit; all but the actual fee (about `100000`) is refunded to the sender.\n\n1. Check the payer's balance. An empty argument file means the node's own payer:\n\n   ```bash\n   : > self.arg\n   logosctl --json call liblogos_lez_rln_module get_native_balance @$PWD/self.arg\n   ```\n\n   **Expected result:** `balance` is at or above the amount you sent.\n\n## Step 6: Wait for the membership to register\n\nThe node registers itself on the LEZ registry, typically one to three minutes after the funding lands.\n\n1. Poll the membership state until `state` is `active`:\n\n   ```bash\n   logosctl --json call liblogos_rln_module get_membership_state $REGISTRY @$PWD/rln-id.arg\n   ```\n\n   - While registering, the reply shows `\"state\":\"unknown\"` and `\"provisioning\":{\"step\":\"awaiting_funding\"}` or `\"step\":\"registering\"`.\n   - Once the registration is submitted, the reply shows `\"state\":\"pending\"` with a `leaf_index`, until the transaction is in a LEZ block.\n\n   **Expected result:** `\"state\":\"active\"` (or `\"grace_period\"`), with a `leaf_index` and a `membership_hash`. `\"state\":\"failed\"` means registration failed; see [Troubleshooting](#troubleshooting-rln-membership). If `daemon.log` says `no membership within 300s`, the module only stopped waiting: check that the sequencer is producing blocks with `getLastBlockId`, and keep polling.\n\n## Step 7: Start the node and send a message\n\nThe node only starts with an active membership.\n\n1. Start the node and check that it reported `nodeStarted`:\n\n   ```bash\n   logosctl --json call delivery_module start\n   until grep nodeStarted events.jsonl; do sleep 1; done\n   ```\n\n1. Wait about 15 seconds for the node to join the [Relay](https://docs.logos.co/get-started/glossary.md#relay) mesh, then subscribe to a [content topic](https://docs.logos.co/get-started/glossary.md#content-topic):\n\n   ```bash\n   sleep 15\n   TOPIC=/my-app/1/hello/proto\n   logosctl --json call delivery_module subscribe $TOPIC\n   ```\n\n1. Note the epoch quota before sending. The `str:` prefix passes the timestamp as a string:\n\n   ```bash\n   logosctl --json call liblogos_rln_module get_epoch_quota $REGISTRY @$PWD/rln-id.arg str:$(date +%s)\n   ```\n\n   **Expected result:** `{\"epoch_index\":N,\"rate_limit\":N,\"remaining\":N}`.\n\n1. Send a message:\n\n   ```bash\n   printf '%s' \"hello from my node\" > payload.arg\n   logosctl --json call delivery_module send $TOPIC @$PWD/payload.arg\n   ```\n\n   **Expected result:** `\"success\":true`, with a `requestId` as the value.\n\n1. Check that the node generated a proof and the message propagated:\n\n   ```bash\n   grep -iE 'rlnGenerateProofRequest|messagePropagated|messageError' events.jsonl\n   ```\n\n   **Expected result:** within about 30 seconds, a proof-generation request for the send (`rlnGenerateProofRequest` or `dispatchRlnGenerateProofRequestEvent`), then:\n\n   ```text\n   {\"event\":\"messagePropagated\",\"data\":{\"arg0\":\"<requestId>\",\"arg1\":\"<message hash>\"}}\n   ```\n\n   - A `messageError` event with your `requestId` means the send failed; its data carries the reason.\n\n1. Run the `get_epoch_quota` call again.\n\n   **Expected result:** in the same `epoch_index`, `remaining` is lower than before the send.\n\n1. Stop the daemon when you're finished:\n\n   ```bash\n   logosctl daemon stop\n   ```\n\n   - The wallet, membership, and installed packages stay in `wallet-home/` and `session/`. To run the node again, start the daemon with the same environment as in [Step 2](#step-2-start-the-daemon-with-a-wallet-home), then `module load delivery_module`, start the event watcher, run `createNode` with the same `node.json`, and `start` once `rlnState` is `Ready`. The existing membership is picked up; nothing is registered or paid again.\n\n## Troubleshooting RLN membership\n\n### A string argument arrives as a number\n\n`logosctl call` converts arguments that look numeric, which can catch a hex RLN identifier, a hash, or a payload. Write the value to a file with no trailing newline (`printf '%s'`) and pass it as `@/absolute/path`; the file's contents are passed verbatim as a string. A `str:<value>` prefix also forces a string.\n\n### `createNode` fails with an unknown preset\n\nSpell the preset exactly `logos.test`. `logostest` is rejected, not treated as an alias.\n\n### `rlnState` stays `Disabled`\n\nThe installed `delivery_module` is older than 0.3.0, where `logos.test` has RLN turned off. Check the version with `logosctl package ls` and upgrade with `logosctl package install delivery_module -y`.\n\n### `rlnState` reports `Failed`\n\nThe reply carries the reason. A common one is a hand-written `wallet_config.json`, or a `LEZ_RLN_SEQUENCER` variable, pointing at a sequencer that isn't the testnet one, so the registry can't be found on-chain. Start with an empty `wallet-home/` to let the module pick the sequencer. The module reads `LEZ_RLN_SEQUENCER` only when `wallet-home/` has no `wallet_config.json` yet, and never rewrites an existing one.\n\nAnother is a `delivery_module` older than 0.3.3, whose `logos.test` preset names the old registry `logos:testnet:841312e9…`, which is no longer used. Check the version with `logosctl package ls`, and upgrade with `logosctl package install delivery_module -y`.\n\n### `wallet_status` reports `failed`, or `daemon.log` shows `Unexpected variant tag`\n\nThe installed `liblogos_lez_rln_module` is older than 5.1.0 and links a LEZ v0.3.0 wallet, which can't read LEZ v0.3.1 blocks. The log line reads `Sync failed: Parse error: Unexpected variant tag`. Run `logosctl catalog refresh`, upgrade with `logosctl package install liblogos_lez_rln_module -y`, and start again with a fresh `wallet-home/`.\n\n### `wallet_status` stays `pending` with the network selected\n\nThe node can't reach the testnet sequencer. Check it from the same machine, using the `sequencer_addr` from `wallet-home/wallet_config.json` (`https://testnet.lez.logos.co/` with current modules); this request should answer with a block number:\n\n```bash\ncurl -sS -X POST -H 'Content-Type: application/json' \\\n  --data '{\"jsonrpc\":\"2.0\",\"method\":\"getLastBlockId\",\"params\":[],\"id\":1}' \\\n  https://testnet.lez.logos.co/\n```\n\n### `wallet_status` stays `pending` with `no network selected`\n\nNothing has named a registry yet. Run `createNode` with the `logos.test` preset as in [Step 4](#step-4-create-the-delivery-node). If it persists, the installed RLN modules are too old; see [Step 3](#step-3-install-and-load-the-modules).\n\n### The wallet never becomes ready, or opens in the wrong place\n\nThe wallet home variables were not set on the `daemon start` command; the modules only see the daemon's environment. Stop the daemon and start it again with them.\n\n### Registry calls fail with `bound to network`\n\n`wallet-home/` was used for another network before. A wallet home bound to one network refuses another. Use a fresh wallet home per network.\n\n### `provisioning.step` stays at `awaiting_funding`\n\nThe node waits indefinitely for its payer balance. Check `get_native_balance` and complete the funding transfer.\n\n### Registration fails with `Incorrect fee`\n\nThe payer holds less than the price plus the fee reserve (`182800000` in total at the minimum fee). Top it up to `250000000`.\n\n### Registration never finishes and `daemon.log` contains `outside registry bounds`\n\nThe registry rejected the requested rate limit. The log line names the allowed range.\n\n### `get_epoch_quota` returns `rate_limit: 0`\n\nThe node has no usable membership yet; the quota is not exhausted. Wait for `get_membership_state` to report `active`.\n\n### `send` fails with `no_usable_membership`\n\nRegistration has not finished. Wait for `get_membership_state` to report `active`.\n\n### Receivers drop the first message after registration\n\nA newly published tree root takes up to one root window to reach other nodes. Later sends are unaffected.\n\n### `daemon start` fails, or `daemon status` reports `not_running`\n\nRead `session/daemon/startup.err` and `session/logs/daemon.log`.\n\n### Every command fails with `no daemon`\n\nThe command isn't using the daemon's session. Check that `LOGOSCTL_CONFIG_DIR` and `TMPDIR` match the ones the daemon was started with; the daemon's local socket lives in `TMPDIR`. Don't run the daemon under a private `/tmp`, such as systemd's `PrivateTmp=yes`.\n\n### `package install` does nothing and asks for confirmation\n\nWithout a terminal, `package install` refuses to proceed unless given `-y`.\n\n### `package install` can't find `liblogos_rln_module`\n\nThe session's catalogue index is stale. Run `logosctl catalog refresh`. `logosctl catalog ls` lists the enabled catalogues; the official one, `logos-modules-official`, must be enabled.\n\n### After a restart the membership reads `unknown` and sends fail\n\nA module process from the previous daemon outlived `daemon stop` and still holds the node's ports and the RLN keystore. Stop the daemon, look for processes left over from the previous daemon with `ps -ef | grep logos`, end them, and start again.\n"},{"id":"lez/extensions/admin-authority","title":"Gate program instructions with admin-authority","summary":"This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/extensions/admin-authority","markdown_url":"https://docs.logos.co/lez/extensions/admin-authority.md","permalink":"/lez/extensions/admin-authority","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/extensions/admin-authority.md","headings":[{"depth":2,"text":"When to use it"},{"depth":2,"text":"Prerequisites"},{"depth":2,"text":"Add the dependency"},{"depth":2,"text":"Install the `spel` CLI"},{"depth":2,"text":"Annotate the module"},{"depth":2,"text":"Gate an instruction"},{"depth":2,"text":"Build and deploy the program"},{"depth":2,"text":"Become the first admin"},{"depth":2,"text":"Fix the first admin at build time"},{"depth":2,"text":"Transfer admin to another party"},{"depth":2,"text":"Hand over in two steps"},{"depth":2,"text":"Use a program (PDA) as the admin"},{"depth":2,"text":"Embedded mode, the admin slot inside your own account"},{"depth":2,"text":"Renounce admin permanently"},{"depth":2,"text":"Verify your integration"},{"depth":2,"text":"Security notes"},{"depth":2,"text":"Reference"}],"markdown":"# Gate program instructions with admin-authority\n\n> **Warning**\n>\n> This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.\n>\n> The dependency snippets pin `admin-authority` v0.2.0 and the framework at the upstream `v0.7.0` tag, the first release that carries the extension mechanism ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)).\n\n`admin-authority` is a SPEL extension that adds a single transferable admin role to your [LEZ](https://docs.logos.co/get-started/glossary.md#lez) program. The admin is the only [account](https://docs.logos.co/get-started/glossary.md#account) allowed to call admin-gated instructions. The role can be transferred to another signer or [PDA](https://docs.logos.co/get-started/glossary.md#pda), or renounced permanently. This page walks through using `admin-authority` from an app developer's perspective. If you are building a different extension, see [Build a SPEL extension library](https://docs.logos.co/lez/extensions/build-a-spel-extension-library.md) instead.\n\n## When to use it\n\nPick `admin-authority` when your [program](https://docs.logos.co/get-started/glossary.md#program) has:\n\n- A configuration or policy account that only one party should mutate (`set_fee_bps`, `update_oracle_address`, `pause`).\n- An emergency action that needs guarded access (`recover_funds`, `migrate_state`).\n- A handoff scenario where ownership might rotate between parties over time.\n\nIf your program needs multi-party approval rather than single-admin gating, `admin-authority` is the wrong primitive, wait for `multisig-authority` (RFP-TBD) or compose admin-authority with a multisig PDA as the admin.\n\n## Prerequisites\n\nYou need a stable Rust toolchain, git, and the native build tools the dependency tree leans on. The `spel` CLI additionally needs `unzip` (the `rust-rapidsnark` build script downloads a prebuilt zip and unpacks it through a helper shell script), the Python development library (a transitive dependency links against libpython through `pyo3`), and the PC/SC smart card library (the wallet crate it builds links `pcsc`). The verification step at the end uses `jq`. On a fresh Ubuntu 24.04 this covers everything:\n\n```bash\nsudo apt-get update && sudo apt-get install -y curl git build-essential pkg-config libssl-dev ca-certificates unzip python3 python3-dev cmake libpcsclite-dev jq\n```\n\nTwo different floors apply here, and neither comes from `admin-authority` itself, which declares only `rust-version = \"1.88\"`. Building your program needs rustc 1.90 or newer whenever `ruint` resolves freely, the floor its 1.20.0 release sets. A `spel init` scaffold pins `ruint = \"=1.17.0\"` in its guest manifest, so that particular floor does not apply on the scaffold route. Installing the `spel` CLI needs rustc 1.94.0 or newer: a transitive `logos-blockchain` crate uses `strict_overflow_ops`, which older toolchains reject as an unstable feature. The CLI pins `channel = \"1.94.0\"` in its own `rust-toolchain.toml`, but `cargo install --git` does not apply a dependency's toolchain file, so your default toolchain has to meet that floor itself. Verified in a clean Ubuntu 24.04 container on rustc 1.99.0: the CLI installs from the tag, the scaffolded program builds and deploys, and every command on this page ran against a local LEZ v0.2.4 sequencer, both handovers included. The deploy and the lifecycle commands, both handovers and renounce included, also ran on the public legacy testnet.\n\n## Add the dependency\n\nIn your program's `Cargo.toml`. If you do not have a program crate yet, do [Install the `spel` CLI](#install-the-spel-cli) and the `spel init` command in [Annotate the module](#annotate-the-module) first, then come back here. For a `spel init` scaffold the manifest to edit is `methods/guest/Cargo.toml`, not the root manifest, which is a `[workspace]` excluding `methods/guest`. That guest manifest already carries its own `[dependencies]` table, holding `spel-framework`, `nssa_core`, `risc0-zkvm`, your project's `_core` crate, `serde`, `borsh` and the `ruint = \"=1.17.0\"` pin from the prerequisites, so merge the entries below into it rather than appending a second table, which cargo rejects outright with `error: duplicate key`. Scaffolded with the source flags from [Annotate the module](#annotate-the-module), its `spel-framework` and `nssa_core` pins already match these, so `admin-authority` is the only line you add:\n\n```toml\n[dependencies]\nadmin-authority = { git = \"https://github.com/mmlado/spel-admin-authority\", tag = \"v0.2.0\" }\nspel-framework  = { git = \"https://github.com/logos-co/spel\", tag = \"v0.7.0\" }\nnssa_core = { git = \"https://github.com/logos-blockchain/logos-execution-zone.git\", tag = \"v0.2.4\", package = \"lee_core\" }\nborsh = { version = \"1\", features = [\"derive\"] }\nserde = { version = \"1\", features = [\"derive\"] }\n```\n\nAll five are needed: the reference samples use exactly this set. `nssa_core` carries the on-chain account types, `borsh` encodes your state, and `serde` is required by the instruction plumbing even when your own types never touch it. The `admin-authority-macros` sub-crate is pulled in transitively. You do not need to declare it directly.\n\nThe `spel-framework` entry must be the reference `admin-authority` itself pins, the `v0.7.0` tag, spelt the same way. The library README names it for each release. Cargo identifies a git dependency by its reference, so a branch, another tag, or even a `rev` pointing at the same commit puts two copies of the framework into your dependency graph, and the build fails with a `From<AdminError>` trait error plus name resolution errors inside the `require_admin` expansion.\n\nAfter adding the dependencies, run `cargo fetch` once from `methods/guest`, not from the project root. The scaffold's root `Cargo.toml` excludes `methods/guest`, so a root `cargo fetch` resolves a graph that does not contain `admin-authority`: it still updates the workspace's own registry and git sources and exits 0, so nothing in its output tells you the extension was skipped. The framework's extension scanner resolves your dependency graph with an offline metadata call, which fails deterministically for a fresh consumer whose git dependencies were never fetched.\n\n## Install the `spel` CLI\n\nThe lifecycle commands below and the IDL check at the end use the `spel` CLI. Install it from the same upstream tag the framework dependency pins:\n\n```bash\ncargo install --git https://github.com/logos-co/spel --tag v0.7.0 spel\n```\n\nThe package name is `spel`, not `spel-cli` as the repository directory suggests, asking cargo for `spel-cli` fails with \"could not find `spel-cli`.\"\n\nThis build also reaches the network beyond fetching crates: a transitive build script downloads a prebuilt `logos-blockchain-circuits` artifact from GitHub releases. It does not honour the usual CA environment variables, so behind a TLS-inspecting proxy it fails with `invalid peer certificate: UnknownIssuer` partway through the build. If the download cannot reach GitHub directly, either point `LBC_ROOT_DIR` at a local circuits build, or pre-seed the cache using a tool that does honour your CA bundle:\n\n```bash\nmkdir -p ~/.cache/logos/blockchain\ncurl -fL https://github.com/logos-blockchain/logos-blockchain-circuits/releases/download/v0.5.3/logos-blockchain-circuits-v0.5.3-linux-x86_64.tar.gz \\\n  | tar xz -C ~/.cache/logos/blockchain/\n```\n\nThe version has to match the `logos-blockchain-circuits` version the pinned framework resolves to, `v0.5.3` for `v0.7.0`. The build script reuses any artifact directory it finds there and skips the download entirely.\n\n## Annotate the module\n\nThis page assumes you already have an LEZ program crate. A deployable one is a RISC Zero guest, and `spel init` scaffolds it at `methods/guest/src/bin/<name>.rs`, where `<name>` is the project name with hyphens replaced by underscores, so `spel init my-program` writes `my_program.rs`. That is the same layout `spel generate-idl` auto-detects when given no path. Pass both source flags ahead of the project name, the parser stops reading flags at the first argument that is not one, so anything after the name is dropped without a warning. With the default pins the scaffold takes `logos-co/spel` at its `main` branch. A branch never unifies with the tag `admin-authority` pins, and as it moves its `lee_core` can differ too. Either way the guest ends up with two copies of the framework and fails to compile with type errors out of the `#[lez_program]` expansion rather than anything naming the marker. Pass the tag:\n\n```bash\nspel init \\\n    --spel-git https://github.com/logos-co/spel.git \\\n    --spel-tag v0.7.0 \\\n    my-program\n```\n\nIf you only want to check the integration and started from `cargo new`, delete the default `fn main` first. The `#[lez_program]` macro generates the program's entry point, and the leftover stub collides with it as a duplicate `main`.\n\nAdd `#[admin_authority]` to your `#[lez_program]` module, directly below the `#[lez_program]` attribute:\n\n```rust\nuse spel_framework::prelude::*;\n\n#[lez_program]\n#[admin_authority]\nmod my_program {\n    #[instruction]\n    pub fn create_pool(\n        #[account(init, pda = literal(\"pool_config\"))] config: AccountWithMetadata,\n    ) -> SpelResult { /* writes the initial PoolConfig */ }\n}\n```\n\nNothing is imported from the library at this point. The `#[admin_authority]` marker is consumed by the framework's scanner during expansion, not resolved as an import, so importing the name only earns an unused import warning. The gate attribute gets imported when the first instruction uses it, next section. The module body does not need `use super::*;` either, the macro resolves paths to items declared outside the module on its own.\n\nThat single annotation exposes five new instructions in your program's IDL:\n\n| Instruction | Purpose |\n|---|---|\n| `admin_initialize` | Creates the admin Config PDA and installs the caller as the first admin. Must be called once after deployment. |\n| `admin_transfer` | Replaces the current admin with a new signer or PDA in one transaction. |\n| `admin_nominate` | Names the next admin. Nothing changes until the nominee accepts. |\n| `admin_accept` | The nominee signs alone and becomes admin. |\n| `admin_renounce` | Zeros the admin permanently. Terminal, no recovery path. |\n\n> **Warning**\n>\n> **Initialisation window.** Until `admin_initialize` is called, the admin Config PDA does not exist. Unless you build with a genesis key, see [Fix the first admin at build time](#fix-the-first-admin-at-build-time), anyone who submits the first `admin_initialize` becomes the admin. Send it as the very next transaction after deployment to prevent a third party from claiming the role. Bundling with the deployment itself is not possible today because a LEZ deployment transaction carries no instructions.\n\n## Gate an instruction\n\nAdd `#[require_admin]` to any instruction that should only succeed when the caller is the current admin:\n\n```rust\n#[account_type]\n#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]\npub struct PoolConfig {\n    pub fee_bps: u32,\n}\n\n// ... inside the #[lez_program] module:\nuse admin_authority::require_admin;\n\n#[instruction]\n#[require_admin]\npub fn set_fee_bps(\n    #[account(mut, pda = literal(\"pool_config\"))] mut config: AccountWithMetadata,\n    new_fee_bps: u32,\n) -> SpelResult {\n    // The admin check has already run.\n    PoolConfig { fee_bps: new_fee_bps }.write_to(&mut config)?;\n    Ok(SpelOutput::execute(vec![config], vec![]))\n}\n```\n\nThe `write_to` helper is yours to write, the library does not provide it. The reference sample uses this one:\n\n```rust\nimpl PoolConfig {\n    fn write_to(&self, account: &mut AccountWithMetadata) -> Result<(), SpelError> {\n        account.account.data = borsh::to_vec(self)\n            .map_err(|_| SpelError::SerializationError { message: \"encoding failed\".into() })?\n            .try_into()\n            .map_err(|_| SpelError::SerializationError { message: \"data too large\".into() })?;\n        Ok(())\n    }\n}\n```\n\nKeep the arguments of instructions you call from the `spel` CLI to the types `spel --help` lists, such as `u8`, `u32`, `u64` and byte arrays. It has no encoding for `u16` and refuses such a call before sending it.\n\nThe `#[account_type]` struct sits outside the `#[lez_program]` module, the instruction inside it. The handler returns `Ok(SpelOutput::execute(post_states, messages))`, where `post_states` lists your declared accounts in declaration order. The injected `admin_config` and `caller` are prepended to the post-states automatically, ahead of the accounts you declared, you only handle the parameters you wrote.\n\nThe gate needs two accounts, the `admin_config` PDA holding the current admin state and a signing `caller`. You do not have to write them: the framework injects both from metadata the library declares, and they appear in the IDL like declared parameters. Declaring them explicitly produces the same program only if you declare them ahead of your own parameters and in the injected order, `admin_config` then `caller`. They are then your parameters, appearing in your post-states list like any other account. Declaring them after your own parameters still compiles, but it reorders the instruction's accounts in the IDL, and that order is the transaction ABI.\n\nIf your instruction already has parameters by different names, point the gate at them with the inject-account names as keys: `#[require_admin(admin_config = my_cfg, caller = owner)]`. Use that form whenever either name differs. The framework does reuse a declared `#[account(signer)]` parameter, or a PDA parameter with the matching seed, in the instruction's account list rather than injecting it twice, but the gate's prologue still refers to the accounts by their role names, so a bare `#[require_admin]` over a renamed parameter fails to compile with `cannot find value caller in this scope` pointed at the attribute.\n\n## Build and deploy the program\n\nThe scaffold's `make build` runs `cargo risczero build`, which compiles the guest in Docker. With framework `v0.7.0` that build fails for a program whose extensions are git dependencies: inside Docker the extension scanner cannot resolve the dependency graph offline, and it stops with `marker(s) [\"admin_authority\"] matched no discoverable extension and dependency resolution failed`. Build natively through the scaffold's `methods` crate instead. That needs the RISC Zero toolchain:\n\n```bash\ncurl -L https://risczero.com/install | bash\nrzup install\n```\n\nTell the `methods` crate which guest to build, in `methods/Cargo.toml`:\n\n```toml\n[package.metadata.risc0]\nmethods = [\"guest\"]\n```\n\nThen build from the project root and read the program id:\n\n```bash\ncargo build --release -p my_program-methods\nspel program-id target/riscv-guest/my_program-methods/my_program-guest/riscv32im-risc0-zkvm-elf/release/my_program.bin\n```\n\nThe value under `ImageID (hex bytes)` is the program id every command below takes as `--program`. Deploy the same file with the LEZ `wallet` CLI, installed from the LEZ release your `nssa_core` pin names. The wallet and the `spel` CLI both read their configuration, including the sequencer address, from the directory `LEE_WALLET_HOME_DIR` points at.\n\nThe public LEZ legacy testnet runs v0.2.4, the release this page builds against. Point a new wallet home at its sequencer, check the connection, and deploy:\n\n```bash\ncargo install --git https://github.com/logos-blockchain/logos-execution-zone.git --tag v0.2.4 wallet\nexport LEE_WALLET_HOME_DIR=~/lez-wallet\nwallet change-network https://testnet-legacy.lez.logos.co\nwallet check-health\nwallet deploy-program target/riscv-guest/my_program-methods/my_program-guest/riscv32im-risc0-zkvm-elf/release/my_program.bin\n```\n\nOn a new wallet home, `change-network` first sets up the wallet. It asks for a password and prints a recovery phrase. The [legacy testnet explorer](https://explorer.testnet-legacy.lez.logos.co/) shows the blocks.\n\nBlocks on the legacy testnet are about a minute apart, so a command can report `Transaction NOT confirmed: Transaction not found in preconfigured amount of blocks` from `spel`, or `Error: All pollers failed` from `wallet auth-transfer init`, and still land a block later. `wallet account get --account-id Public/<account-id>` shows the `nonce` of a key, which goes up by one when a transaction it signed lands. Wait for that before you send the next transaction from the same key. A transaction built while the previous one is still pending reuses its nonce and does not land.\n\nTo test against a local sequencer instead, build the standalone sequencer from the same LEZ release and start it in its own terminal. It listens on `127.0.0.1:3040` and keeps its state in the directory you start it from:\n\n```bash\ngit clone --branch v0.2.4 https://github.com/logos-blockchain/logos-execution-zone.git ~/lez\n(cd ~/lez && cargo build --release --features standalone -p sequencer_service)\nmkdir -p ~/lez-sequencer && cp ~/lez/lez/sequencer/service/configs/debug/sequencer_config.json ~/lez-sequencer/\ncd ~/lez-sequencer && RUST_LOG=info ~/lez/target/release/sequencer_service ./sequencer_config.json\n```\n\nIn another terminal, from the project root, point a wallet home at that sequencer and deploy:\n\n```bash\nmkdir -p ~/lez-wallet-local && cp ~/lez/lez/wallet/configs/debug/wallet_config.json ~/lez-wallet-local/\nexport LEE_WALLET_HOME_DIR=~/lez-wallet-local\nwallet check-health\nwallet deploy-program target/riscv-guest/my_program-methods/my_program-guest/riscv32im-risc0-zkvm-elf/release/my_program.bin\n```\n\n## Become the first admin\n\n`admin_initialize` takes no arguments. The signing caller becomes the admin (self-election). There is no candidate argument at initialise because the LEZ duplicate-account rule rejects a transaction listing the same account twice, so a caller could never also pass itself as candidate evidence.\n\nThe admin key must be owned by a program. A key that has never been used is owned by none, and on LEZ v0.2.1 to v0.2.4 such a key can sign once and is then rejected in every later transaction. `admin-authority` therefore refuses it as admin with program error 6112, `admin key is owned by no program`. If your wallet has no public account yet, create one. It prints `Generated new account with account_id Public/<your-account-id>`, and the commands below take the id without the `Public/` prefix. Then claim the key:\n\n```bash\nwallet account new public\nwallet auth-transfer init --account-id Public/<your-account-id>\n```\n\nEvery lifecycle command below reads your program's IDL from a file. Generate it once from the program you just built:\n\n```bash\n# From a `spel init` project root, drop the path entirely and the guest is auto-detected.\nspel generate-idl path/to/your/program/src/main.rs > program-idl.json\n```\n\nTo check a command without submitting it, add `--dry-run=text` before the `--` separator. It resolves the accounts and arguments and prints the transaction it would send, which is the only way to validate an invocation before you have a node to submit to.\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-initialize --caller <your-account-id>\n```\n\nWhen the sequencer rejects a transaction, the CLI only reports `Transaction NOT confirmed: Transaction not found in preconfigured amount of blocks`. The reason is in the sequencer log as program error 6000 plus the library's error code, for example `Program error [6103]: ... signer is not the current admin`. The public testnet does not publish its log, so to see the code, run the failing call against a local sequencer. The [library README](https://github.com/mmlado/spel-admin-authority#errors) lists the codes.\n\nTo hand the role to a different keyholder or a PDA, initialise first and then call `admin_transfer`, or nominate a successor, see [Hand over in two steps](#hand-over-in-two-steps).\n\n## Fix the first admin at build time\n\nSet `SPEL_ADMIN_GENESIS` to an account id, as 64 hex characters, when the program is built, and only that account can run `admin_initialize`. Anyone else is rejected with program error 6115, `signer is not this build's genesis admin`, which closes the initialisation window. Put it in a `.cargo/config.toml` in the project root, where every build picks it up:\n\n```toml\n[env]\nSPEL_ADMIN_GENESIS = \"<64 hex characters>\"\n```\n\nThe key is compiled into the program, so the program id changes with it. Compare `spel program-id` with and without the key to confirm it is in. A malformed or all-zero value fails the build. In embedded mode there is no `admin_initialize`: call `admin_authority::require_genesis(&caller)?` in your account-creating instruction for the same check.\n\n## Transfer admin to another party\n\n`admin_transfer` requires the current admin to sign. It takes an `AdminCandidate` describing the new admin, paired with a matching `AccountWithMetadata` that proves the candidate on chain.\n\nTwo candidate shapes:\n\n```rust\n// `AdminCandidate` is a re-exported alias for the shared authority type:\n// `pub type AdminCandidate = AuthorityCandidate;`\npub enum AuthorityCandidate {\n    /// The new admin is a keyholder. Validated by checking the new account\n    /// co-signed the transaction.\n    Signer,\n    /// The new admin is a program-owned PDA. Validated by deriving the address\n    /// from (program_id, seed) and confirming the PDA exists on chain.\n    Pda { program_id: ProgramId, seed: [u8; 32] },\n}\n```\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-transfer \\\n    --caller <current-admin-account-id> \\\n    --new-account <new-admin-account-id> \\\n    --candidate Signer\n```\n\nA `Signer` transfer needs the new admin's signature on the same transaction, which proves the keyholder consents. That means two parties sign one message. Run as written, the command above builds and submits with the caller's signature only, and the sequencer drops the transaction unless the candidate's signature is attached. The `spel` CLI collects that second signature through a partial-transaction file ([logos-co/spel#246](https://github.com/logos-co/spel/pull/246)). The current admin adds the global flags `--co-signer` and `--export` before the `--` separator, which writes the transaction to a file instead of submitting it. The new admin signs it in their own wallet, offline, and anyone with a node connection submits it:\n\n```bash\nspel --co-signer <new-admin-account-id> --export handover.json \\\n    --idl program-idl.json --program <program-id> -- \\\n    admin-transfer \\\n    --caller <current-admin-account-id> \\\n    --new-account <new-admin-account-id> \\\n    --candidate Signer\n\n# The new admin, in their own wallet:\nspel sign handover.json\n\n# Anyone with a node connection:\nspel submit handover.json\n```\n\n`spel sign` prints the decoded instruction and asks for confirmation before it adds the signature. The file records the next `nonce` of both keys, so neither key may send another transaction between the export and the submit, otherwise the sequencer rejects the transfer. The new admin key needs a program owner, like the first admin's. The witness exchange section of the CLI README describes the file format.\n\nAfter the transaction lands, the previous admin can no longer call gated instructions.\n\n## Hand over in two steps\n\n`admin_nominate` and `admin_accept` move the role without both parties signing one transaction. The current admin names the next admin, and the nominee later accepts on their own signature:\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-nominate --caller <current-admin-account-id> --new-admin <nominee-account-id-hex>\n\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-accept --caller <nominee-account-id>\n```\n\n`--new-admin` is a raw 32 byte argument, pass the account id as 64 hex characters, not the base58 form the wallet prints. Convert a base58 id with:\n\n```bash\npython3 -c 'import sys,functools;a=\"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";print(functools.reduce(lambda n,c:n*58+a.index(c),sys.argv[1],0).to_bytes(32,\"big\").hex())' <account-id>\n```\n\nNothing changes until the nominee accepts, so a wrong id only costs a second nomination. A later nomination replaces an earlier one, nominating the all-zero id cancels, and `admin_transfer` and `admin_renounce` both clear a pending nominee. The nominee key needs a program owner. A PDA nominee accepts through its owning program, which chained-calls `admin_accept` with the seeds of the PDA.\n\n## Use a program (PDA) as the admin\n\nTo delegate admin authority to another program, for example a multisig, use `AdminCandidate::Pda` with the delegating program's ID and PDA seed. Payload variants are passed to the CLI as a one-key JSON object:\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-transfer \\\n    --caller <current-admin-account-id> \\\n    --new-account <pda-account-id> \\\n    --candidate '{\"Pda\": {\"program_id\": \"<multisig-program-id>\", \"seed\": \"<32-byte-hex-seed>\"}}'\n```\n\nThe PDA must already exist on chain as a claimed account, an unclaimed candidate is rejected. When the multisig later wants to invoke a gated instruction on your program, it does so through a chained call and declares its admin PDA in `caller-pda-seeds`. LEZ verifies the seed and propagates `is_authorized = true` to your program; the `#[require_admin]` check then accepts the PDA as the legitimate admin. No private key is needed for the PDA, authorisation comes from the seed delegation.\n\n## Embedded mode, the admin slot inside your own account\n\nInstead of a dedicated Config PDA, the admin slot can live inside one of your program's own accounts at a byte offset. Declared once, program wide, on the marker:\n\n```rust\nuse admin_authority::AdminConfig;\n\n#[account_type]\n#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]\npub struct ProgramConfig {\n    pub value: u64,           // bytes 0..8\n    pub padding: [u8; 24],    // bytes 8..32\n    #[admin_slot]\n    pub admin: AdminConfig,   // bytes 32..96, the embedded slot\n}\n\n#[lez_program]\n#[admin_authority(admin_config = config, offset = 32)]\nmod my_program {\n    use admin_authority::admin_initialize;\n\n    #[admin_initialize]\n    #[instruction]\n    pub fn initialize(\n        #[account(init, pda = literal(\"program_config\"))] mut config: AccountWithMetadata,\n    ) -> SpelResult {\n        ProgramConfig {\n            value: 0,\n            padding: [0; 24],\n            admin: AdminConfig::default(),\n        }\n        .write_to(&mut config)?;\n        // the signing caller is injected, and the injected bootstrap\n        // installs it as admin in this same transaction\n        Ok(SpelOutput::execute(vec![config], vec![]))\n    }\n}\n```\n\n`write_to` is the same helper pattern from the gating section, implemented on `ProgramConfig`.\n\nWhat changes:\n\n- **No `admin_initialize` instruction.** Mark your own account-creating instruction with `#[admin_initialize]` instead. The bootstrap is injected, the caller is installed as admin in the same transaction that creates the account, and the slot is born initialised, so the initialisation window from the warning above does not exist in embedded mode. An account created without the bootstrap is born renounced, permanently.\n- **`#[admin_slot]` keeps the layout honest.** The field marker derives an `ADMIN_SLOT_OFFSET` const and a layout test, and the build fails if the marker position and the `offset = ...` declaration ever disagree, for example after a field is added above the slot.\n- **Everything retargets.** Gates read the slot at the declared offset from your account, the management instructions splice only the 64 byte window and leave your neighbouring fields untouched, and the IDL shows your account wherever the dedicated PDA used to appear.\n- **The offset is never in a transaction.** It compiles into the program as a literal. The IDL carries no offset argument, and writing `admin_config = ...` or `offset = ...` on a gate by hand is a compile error in embedded mode.\n- **One account fewer** on every gated transaction, the slot travels with state you were already passing.\n\nThe embedded `AdminConfig` field must sit at the declared offset with only fixed-size fields before it. The library repository ships a reference sample (`admin-authority-sample-embedded`) with the layout, tests, and a committed dry-run walkthrough.\n\n## Renounce admin permanently\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    admin-renounce --caller <current-admin-account-id>\n```\n\nThis writes `AccountId::default()` to the Config PDA and clears any pending nominee. From then on every admin-gated instruction, `admin_transfer`, `admin_nominate` and `admin_accept` reject with program error 6102, `admin authority renounced`. There is no recovery path, design your program so renounce is only callable when permanent loss of mutability is the intended outcome (handoff to \"immutable\" governance, end of life, etc.).\n\n## Verify your integration\n\nAfter building your program, check that the admin instructions appear in the IDL:\n\n```bash\n# From a `spel init` project root, drop the path entirely and the guest is auto-detected.\nspel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'\n```\n\nThe `spel` binary must be built from the same framework revision your `Cargo.toml` pins. A CLI built without the extension scanner omits the admin instructions from this output without reporting an error, so the check appears to pass while the surface is missing. The install command in [Install the `spel` CLI](#install-the-spel-cli) pins the right revision. This check is also not a substitute for a build: `generate-idl` reads your source rather than compiling it, so it emits a complete IDL for a program that does not compile. Run `cargo check` first and treat the IDL as confirmation of the surface, not of the integration.\n\nExpected output includes:\n\n```\n\"admin_initialize\"\n\"admin_transfer\"\n\"admin_renounce\"\n\"admin_nominate\"\n\"admin_accept\"\n```\n\nPlus your own instructions. On a framework build that carries the extension scanner, a marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the admin instructions. That safety net is a property of the framework version: on a framework without the scanner, any upstream release before `v0.7.0`, the marker is ignored and the program builds cleanly without them. When you hit the hard error, the most common causes are:\n\n- `admin-authority` not declared as a direct path or git dependency in your `Cargo.toml`. Transitive dependencies are never discovered.\n- Cached macro expansion, run `cargo clean -p <your-crate>` and rebuild.\n\nMisplacing the marker is a different failure and never reaches that error, because a marker above `#[lez_program]` is consumed before the framework sees it. Since nothing on this page imports `admin_authority`, writing it above `#[lez_program]` stops at name resolution instead, with ``cannot find attribute `admin_authority` in this scope`` and a note that the name is a crate rather than an attribute. Import the name and `admin-authority` v0.2.0 rejects the placement itself: `#[admin_authority] must come after #[lez_program]`.\n\n## Security notes\n\n- **Initialisation window**, front-running is possible until the first `admin_initialize` lands. Build with a genesis key, or send it immediately after deployment. Deploy-time bundling is not possible on LEZ today, a deployment transaction carries no instructions.\n- **Admin keys need an owner**, run `wallet auth-transfer init` on a key before it becomes admin. A key no program owns is refused with program error 6112.\n- **Declared config accounts need their `pda`**, an instruction that declares `admin_config` itself must give it `pda = literal(\"admin_config\")`. The gate trusts the account it is given.\n- **Renounce is terminal**, there is no recovery. Treat it as a one-way switch.\n- **PDA admins via CPI**, the delegating program must declare its admin PDA in `caller-pda-seeds` for the gated call. LEZ verifies the seed; the admin check then trusts the propagated `is_authorized`.\n- **Transfer history**, not recorded on chain in this release. The current admin is always readable from the Config PDA; historical transfers require an off-chain indexer.\n\n## Reference\n\nSource: [github.com/mmlado/spel-admin-authority](https://github.com/mmlado/spel-admin-authority). The companion repository contains the authority lifecycle state diagram, ADRs for design decisions, and a reference sample program demonstrating end-to-end integration.\n"},{"id":"lez/extensions/freeze-authority","title":"Freeze program execution with freeze-authority","summary":"This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/extensions/freeze-authority","markdown_url":"https://docs.logos.co/lez/extensions/freeze-authority.md","permalink":"/lez/extensions/freeze-authority","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/extensions/freeze-authority.md","headings":[{"depth":2,"text":"When to use it"},{"depth":2,"text":"Prerequisites"},{"depth":2,"text":"Add the dependency"},{"depth":2,"text":"Annotate the module"},{"depth":3,"text":"Auto mode (recommended default)"},{"depth":3,"text":"Manual mode"},{"depth":2,"text":"Gate an instruction"},{"depth":3,"text":"Exempt an instruction from auto mode"},{"depth":2,"text":"Initialise the freeze authority"},{"depth":2,"text":"Freeze and unfreeze the program"},{"depth":2,"text":"Freeze and unfreeze a specific account"},{"depth":2,"text":"Transfer freeze authority to another party"},{"depth":2,"text":"Hand over the freeze authority in two steps"},{"depth":2,"text":"Use a program (PDA) as freeze authority"},{"depth":2,"text":"Renounce freeze authority"},{"depth":2,"text":"Embedded mode, the freeze slot inside your own account"},{"depth":2,"text":"Verify your integration"},{"depth":2,"text":"Security notes"},{"depth":2,"text":"Reference"}],"markdown":"# Freeze program execution with freeze-authority\n\n> **Warning**\n>\n> This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.\n>\n> The dependency snippets pin `admin-authority` and `freeze-authority` v0.2.0 and the framework at the upstream `v0.7.0` tag, the first release that carries the extension mechanism ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)).\n\n`freeze-authority` is a SPEL extension that adds an emergency-stop primitive to your [LEZ](https://docs.logos.co/get-started/glossary.md#lez) program. A designated freeze authority can pause all [program](https://docs.logos.co/get-started/glossary.md#program) execution (program-wide freeze) and block specific [accounts](https://docs.logos.co/get-started/glossary.md#account) from interacting (per-account freeze). The role can be transferred by the admin, handed over in two steps, or renounced. While the program is frozen, only the freeze management carve-outs (unfreeze, authority transfer, nomination and renounce, per-account freeze edits), admin operations, and instructions you marked `#[freeze_exempt]` remain callable. This page walks through using `freeze-authority` from an app developer's perspective. If you are building a different extension, see [Build a SPEL extension library](https://docs.logos.co/lez/extensions/build-a-spel-extension-library.md) instead.\n\n`freeze-authority` depends on `admin-authority`. The admin governs the freeze authority slot; the freeze authority governs the frozen flags. See [Gate program instructions with admin-authority](https://docs.logos.co/lez/extensions/admin-authority.md) for the admin layer.\n\n## When to use it\n\nPick `freeze-authority` when your program needs:\n\n- An emergency circuit breaker for incident response (`freeze` everything until the team can investigate).\n- A blocklist for sanctioned or compromised accounts (`block` specific `AccountId`s while the rest of the program keeps running).\n- Both layered, global pause plus per-account blocks for graduated response.\n\nIf your program needs a permanent pause with no recovery, use `admin_renounce` after deployment instead, freeze-authority is the wrong primitive for one-way upgrades.\n\n## Prerequisites\n\nSame toolchain as the admin-authority page: a stable Rust toolchain, git, the native build packages, and the `spel` CLI. See [Prerequisites](https://docs.logos.co/lez/extensions/admin-authority.md#prerequisites) and [Install the `spel` CLI](https://docs.logos.co/lez/extensions/admin-authority.md#install-the-spel-cli) there, plus the `spel init` command in [Annotate the module](https://docs.logos.co/lez/extensions/admin-authority.md#annotate-the-module) if you do not have a program crate yet, and [Build and deploy the program](https://docs.logos.co/lez/extensions/admin-authority.md#build-and-deploy-the-program) to get it on chain. Those are all you need from that page. You do not have to work through the admin integration first: the dependency block below already carries `admin-authority`, and its five instructions arrive with your build. Everything below assumes the toolchain and the CLI are in place.\n\nVerified in a clean Ubuntu 24.04 container on rustc 1.99.0: the auto mode example builds and deploys, the manual and embedded examples compile with the IDL shown below, and every command on this page ran against a local LEZ v0.2.4 sequencer, both handovers included. On the public legacy testnet, the deploy, initialisation, program and account freezes, the two-step handover and renounce also ran as documented. The toolchain floors from the admin-authority page apply here unchanged.\n\n## Add the dependency\n\nIn your program's `Cargo.toml`. If you do not have a program crate yet, run the `spel init` command in [Annotate the module](https://docs.logos.co/lez/extensions/admin-authority.md#annotate-the-module) on the admin page first, then come back here. For a `spel init` scaffold the manifest to edit is `methods/guest/Cargo.toml`, not the root manifest, which is a `[workspace]` excluding `methods/guest`. That guest manifest already carries its own `[dependencies]` table, holding `spel-framework`, `nssa_core`, `risc0-zkvm`, your project's `_core` crate, `serde`, `borsh` and the `ruint = \"=1.17.0\"` pin from the prerequisites, so merge the entries below into that table rather than appending a second one, which cargo rejects with `error: duplicate key`. On that path `admin-authority` and `freeze-authority` are the only two lines you add:\n\n```toml\n[dependencies]\nadmin-authority  = { git = \"https://github.com/mmlado/spel-admin-authority\", tag = \"v0.2.0\" }\nfreeze-authority = { git = \"https://github.com/mmlado/spel-freeze-authority\", tag = \"v0.2.0\" }\nspel-framework   = { git = \"https://github.com/logos-co/spel\", tag = \"v0.7.0\" }\nnssa_core = { git = \"https://github.com/logos-blockchain/logos-execution-zone.git\", tag = \"v0.2.4\", package = \"lee_core\" }\nborsh = { version = \"1\", features = [\"derive\"] }\nserde = { version = \"1\", features = [\"derive\"] }\n```\n\nThe `admin-authority` dependency is required because freeze-authority composes with it, and both must be direct dependencies, the framework never discovers extensions transitively. Use the tags above together, `freeze-authority` v0.2.0 pins `admin-authority` v0.2.0, so a consumer on both resolves one copy of each. The framework must be the tag those releases pin, `tag = \"v0.7.0\"`, spelt the same way. Cargo identifies a git dependency by its reference, so a branch, or a `rev` pointing at the same commit, gives you two copies of the framework and a `From<AdminError>` trait error. `nssa_core` carries the on-chain account types, `borsh` encodes your state, and `serde` is required by the instruction plumbing. The `freeze-authority-macros` sub-crate is pulled in transitively.\n\nAfter adding the dependencies, run `cargo fetch` once from `methods/guest`, not from the project root. The scaffold's root `Cargo.toml` excludes `methods/guest`, so a root `cargo fetch` resolves a graph that contains neither `admin-authority` nor `freeze-authority`: it still updates the workspace's own registry and git sources and exits 0, so nothing in its output tells you the extensions were skipped. The framework's extension scanner resolves your dependency graph with an offline metadata call, which fails deterministically for a fresh consumer whose git dependencies were never fetched. And if you started from `cargo new`, delete the default `fn main`, the `#[lez_program]` macro generates the program's entry point.\n\n## Annotate the module\n\n`freeze-authority` ships two modes: **auto** (default, F3-strict) and **manual** (explicit opt-in per instruction). Both require the admin marker so the freeze authority slot has an owner.\n\n### Auto mode (recommended default)\n\nEvery dispatched instruction except the F3 carve-outs and admin operations is automatically gated by the freeze check. Consumers opt OUT per instruction with `#[freeze_exempt]`.\n\n```rust\nuse freeze_authority::{freeze_exempt, FreezeCandidate};\nuse spel_framework::prelude::*;\n\n#[lez_program]\n#[admin_authority]\n#[freeze_authority]\nmod my_program {\n    #[instruction]\n    #[freeze_exempt]\n    pub fn initialize(/* ... */) -> SpelResult { /* ... */ } // creates your account, see below\n\n    #[instruction]\n    pub fn transfer(/* ... */) -> SpelResult { /* ... */ }   // auto-gated\n\n    #[instruction]\n    #[freeze_exempt]\n    pub fn balance_of(/* ... */) -> SpelResult { /* ... */ } // exempt, callable while frozen\n}\n```\n\nYour own account-creating instruction needs `#[freeze_exempt]` in dedicated mode, because `freeze_config` does not exist yet when it runs and the auto-wrap prologue would reject it as not initialised. In embedded mode the framework works this out for itself, the instruction that creates the embedding account is skipped by that gate automatically.\n\nThe `#[admin_authority]` and `#[freeze_authority]` markers are not imported, the framework's scanner consumes them during expansion, importing those names only earns unused import warnings. `freeze_exempt` and `FreezeCandidate` are real imports, and `FreezeCandidate` is the one that must sit outside the module, where the example puts them. It is required even though your own code never names it, the generated `freeze_authority_transfer` instruction references it at the file's outer scope, so an import inside the module fails with `cannot find type FreezeCandidate in this scope` pointed at `#[lez_program]` rather than at the import. `freeze_exempt` resolves from either position, but dropping it altogether fails with `cannot find attribute freeze_exempt in this scope` on every exempt instruction. The module body does not need `use super::*;`.\n\n### Manual mode\n\nAuto-wrap is disabled; the consumer applies `#[require_not_frozen]` only to instructions they want gated. F3 conformance becomes the consumer's responsibility.\n\n```rust\nuse freeze_authority::{require_not_frozen, FreezeCandidate};\nuse spel_framework::prelude::*;\n\n#[lez_program]\n#[admin_authority]\n#[freeze_authority(manual)]\nmod my_program {\n    #[instruction]\n    #[require_not_frozen]\n    pub fn transfer(/* ... */) -> SpelResult { /* ... */ }   // explicitly gated\n\n    #[instruction]\n    pub fn balance_of(/* ... */) -> SpelResult { /* ... */ } // NOT gated\n}\n```\n\nThat single annotation pair (plus `#[admin_authority]`) exposes nine new instructions in your program's IDL:\n\n| Instruction | Purpose |\n|---|---|\n| `freeze_initialize` | Creates the freeze Config [PDA](https://docs.logos.co/get-started/glossary.md#pda) and sets the first freeze authority. Requires admin signature. Must be called once after deployment. |\n| `freeze_program` | Sets the program-wide frozen flag to true. Freeze authority only. |\n| `freeze_program_release` | Sets the program-wide frozen flag to false. Freeze authority only. Callable while frozen. |\n| `freeze_authority_transfer` | Replaces the current freeze authority with a new signer or PDA in one transaction. Admin only. Callable while frozen. |\n| `freeze_authority_nominate` | Names the next freeze authority. The holder or the admin signs. Callable while frozen. |\n| `freeze_authority_accept` | The nominee signs alone and becomes freeze authority. Callable while frozen. |\n| `freeze_authority_renounce` | Vacates the freeze authority slot. Admin OR freeze authority self. Callable while frozen. Recoverable by admin via transfer. |\n| `freeze_account(target)` | Sets per-account frozen flag to true for `target`. Freeze authority only. Callable while frozen. |\n| `freeze_account_release(target)` | Sets per-account frozen flag to false for `target`. Freeze authority only. Callable while frozen. |\n\n> **Warning**\n>\n> **Initialisation window.** Until `freeze_initialize` is called, the freeze Config PDA does not exist, and in auto mode that blocks the program rather than leaving it open: the injected prologue strict-decodes `freeze_config`, so every non-exempt instruction rejects with program error 6201, `freeze authority not initialized`. Unlike `admin_initialize`, freeze initialisation is NOT front-runnable, `freeze_initialize` requires the admin's signature. But it does require `admin_initialize` to have run first. Recommended pattern: submit `admin_initialize` and then `freeze_initialize` as the first two transactions after deployment, back to back and before any of your own instructions. A LEZ transaction carries a single instruction, so they cannot share one.\n\n## Gate an instruction\n\nIn auto mode, all instructions are gated by default, you don't add any annotation. In manual mode, add `#[require_not_frozen]` to instructions you want gated:\n\n```rust\n#[instruction]\n#[require_not_frozen]\npub fn transfer(\n    #[account(mut, pda = literal(\"balance\"))] mut balance: AccountWithMetadata,\n    #[account(signer)] caller: AccountWithMetadata,\n    amount: u64,\n) -> SpelResult {\n    /* your logic */\n}\n```\n\nThe injected gate performs two checks before the handler body runs:\n\n1. **Program-wide check**, reads `freeze_config.is_frozen`. Rejects if true.\n1. **Per-account check**, derives the PDA at `(program_id, \"frozen\", caller.account_id)` and reads `is_frozen`. Rejects if true. Missing PDA = not frozen.\n\nBoth checks pass for the call to proceed.\n\n### Exempt an instruction from auto mode\n\nUse `#[freeze_exempt]` to opt out per instruction:\n\n```rust\n#[instruction]\n#[freeze_exempt]\npub fn balance_of(/* ... */) -> SpelResult { /* read-only, safe while frozen */ }\n```\n\nThe framework reads `self_exempt_marker = \"freeze_exempt\"` from freeze-authority's Cargo metadata and skips the wrap for any function carrying the attribute.\n\n## Initialise the freeze authority\n\n`freeze_initialize` takes no candidate argument. The admin signs, and the admin becomes the initial freeze authority, the same self-election pattern as `admin_initialize`. Hand the role to a dedicated operations key or a PDA afterwards with `freeze_authority_transfer`.\n\nEvery command below reads `program-idl.json`, generated once from your built program, and takes `--dry-run=text` before the `--` separator to validate an invocation without submitting it. See [Become the first admin](https://docs.logos.co/lez/extensions/admin-authority.md#become-the-first-admin) for both.\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-initialize --caller <admin-account-id>\n```\n\n## Freeze and unfreeze the program\n\n```bash\n# Freeze: rejects every interaction except the F3 carve-outs and admin operations.\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-program --caller <freeze-authority-account-id>\n\n# Unfreeze: restores normal operation.\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-program-release --caller <freeze-authority-account-id>\n```\n\nBoth require the current freeze authority to sign. While the program is frozen, gated instructions reject with program error 6217, `program is frozen`.\n\n## Freeze and unfreeze a specific account\n\n```bash\n# Block account X from interacting with this program.\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-account --caller <freeze-authority-account-id> --target <account-id-X-hex>\n\n# Restore X's access.\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-account-release --caller <freeze-authority-account-id> --target <account-id-X-hex>\n```\n\n`target` is a raw 32 byte argument, pass the account id as 64 hex characters, not base58.\n\nWhen account X is frozen, any instruction in your program that's auto-gated or carries `#[require_not_frozen]` rejects when X is the signer, with program error 6218, `account is frozen`. Other accounts are unaffected. Per-account state survives the program-wide frozen flag toggling, the two layers are independent. Releasing a target that is not currently frozen rejects with program error 6214, `account is not frozen`, so a release cannot silently create marker state for untouched accounts.\n\n## Transfer freeze authority to another party\n\n`freeze_authority_transfer` requires the admin to sign. It takes a `FreezeCandidate` describing the new holder, the same type as `AdminCandidate` (each is an alias for the shared `AuthorityCandidate`, and both libraries pin one copy of that crate), paired with a `new_account` that carries the chain-state evidence.\n\nThe two candidate shapes, `Signer` and `Pda`, are documented in [Transfer admin to another party](https://docs.logos.co/lez/extensions/admin-authority.md#transfer-admin-to-another-party), and the commands below show both in use.\n\nThe slot can also be transferred from a Renounced (vacant) state, so admins can rotate the role with or without an interim vacancy:\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-authority-transfer \\\n    --caller <admin-account-id> \\\n    --new-account <new-freeze-authority-account-id> \\\n    --candidate Signer\n```\n\nA `Signer` candidate is validated on chain by checking that the new holder co-signed the transaction, and the wallet only collects signatures for declared signer accounts. Run as written, the command above builds and submits with the caller's signature only, and the sequencer drops it unless the new holder's signature is attached. The `spel` CLI collects that second signature through a partial-transaction file ([logos-co/spel#246](https://github.com/logos-co/spel/pull/246)): the admin adds the global flags `--co-signer <new-freeze-authority-account-id> --export handover.json` before the `--` separator, the new holder runs `spel sign handover.json` in their own wallet, and anyone with a node connection runs `spel submit handover.json`. Neither key may send another transaction between the export and the submit. See the transfer section of the [admin-authority page](https://docs.logos.co/lez/extensions/admin-authority.md#transfer-admin-to-another-party) for the full commands. The new holder's key needs a program owner, run `wallet auth-transfer init` on it first, otherwise the transfer is refused with program error 6221.\n\n## Hand over the freeze authority in two steps\n\nThe current freeze authority or the admin names the next holder, and the nominee accepts on their own signature:\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-authority-nominate \\\n    --caller <holder-or-admin-account-id> \\\n    --new-freeze-authority <nominee-account-id-hex>\n\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-authority-accept --caller <nominee-account-id>\n```\n\n`--new-freeze-authority` takes the account id as 64 hex characters, not the base58 form the wallet prints. The same goes for `--target` above. [Hand over in two steps](https://docs.logos.co/lez/extensions/admin-authority.md#hand-over-in-two-steps) on the admin page shows how to convert one. Routine rotation needs only the holder, and the admin can nominate over the holder's choice. With the slot vacant only the admin can nominate, so this is also how the admin refills a renounced slot. A later nomination replaces an earlier one, nominating the all-zero id cancels, and transfer and renounce clear a pending nominee. Both instructions stay callable while the program is frozen.\n\n## Use a program (PDA) as freeze authority\n\nTo delegate freeze authority to another program (for example, a multisig or a circuit-breaker DAO), pass `FreezeCandidate::Pda`:\n\n```bash\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-authority-transfer \\\n    --caller <admin-account-id> \\\n    --new-account <pda-account-id> \\\n    --candidate '{\"Pda\": {\"program_id\": \"<multisig-program-id>\", \"seed\": \"<32-byte-hex-seed>\"}}'\n```\n\nWhen the multisig invokes `freeze_program` (or any freeze-authority-signed instruction), it does so through a chained call and declares its PDA in `caller-pda-seeds`. LEZ verifies the seed and propagates `is_authorized = true`; the gate accepts the PDA as the legitimate freeze authority.\n\n## Renounce freeze authority\n\n```bash\n# Either the current admin OR the current freeze authority can sign.\nspel --idl program-idl.json --program <program-id> -- \\\n    freeze-authority-renounce --caller <admin-or-freeze-authority-account-id>\n```\n\nUnlike admin renounce, this is NOT terminal. The freeze authority slot becomes vacant and any pending nominee is cleared. The admin can repopulate it later with `freeze_authority_transfer`, or by nominating a holder who then accepts. While the slot is vacant, `freeze_program`, `freeze_program_release`, `freeze_account`, and `freeze_account_release` all fail with program error 6210, `freeze authority renounced`. The program-wide `is_frozen` flag and per-account states are preserved at the moment of renounce, they don't reset.\n\nIf the admin has already been renounced first (terminal), the freeze slot becomes effectively permanent: there is no one left to repopulate it, unless the admin nominated a holder on the vacant slot before renouncing, who can still accept. Plan the order of renounces carefully if you intend to commit to no-future-freeze.\n\n## Embedded mode, the freeze slot inside your own account\n\nLike admin-authority, the freeze state can live inside one of your program's own accounts instead of a dedicated Config PDA, and both extensions can share the same account at distinct offsets:\n\n```rust\nuse admin_authority::AdminConfig;\nuse freeze_authority::{FreezeCandidate, FreezeConfig};\n\n#[account_type]\n#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]\npub struct ProgramConfig {\n    pub value: u64,            // bytes 0..8\n    pub padding: [u8; 24],     // bytes 8..32\n    #[admin_slot]\n    pub admin: AdminConfig,    // bytes 32..96, the admin slot\n    #[freeze_slot]\n    pub freeze: FreezeConfig,  // bytes 96..161, the freeze slot\n}\n\n#[lez_program]\n#[admin_authority(admin_config = config, offset = 32)]\n#[freeze_authority(freeze_config = config, offset = 96)]\nmod my_program {\n    use admin_authority::admin_initialize;\n\n    #[admin_initialize]\n    #[instruction]\n    pub fn initialize(\n        #[account(init, pda = literal(\"program_config\"))] mut config: AccountWithMetadata,\n    ) -> SpelResult {\n        ProgramConfig {\n            value: 0,\n            padding: [0; 24],\n            admin: AdminConfig::default(),\n            freeze: FreezeConfig::default(),\n        }\n        .write_to(&mut config)?;\n        // the signing caller is injected, the injected bootstrap\n        // installs it as admin, and the freeze slot stays vacant\n        Ok(SpelOutput::execute(vec![config], vec![]))\n    }\n}\n```\n\nThe `write_to` helper is yours to write, the library does not provide it. The reference sample uses this one:\n\n```rust\nimpl ProgramConfig {\n    fn write_to(&self, account: &mut AccountWithMetadata) -> Result<(), SpelError> {\n        account.account.data = borsh::to_vec(self)\n            .map_err(|_| SpelError::SerializationError { message: \"encoding failed\".into() })?\n            .try_into()\n            .map_err(|_| SpelError::SerializationError { message: \"data too large\".into() })?;\n        Ok(())\n    }\n}\n```\n\nWhat changes:\n\n- **No `freeze_initialize`.** Your account-creating instruction writes the struct, and the freeze slot is born vacant: it rejects every holder-path caller until the admin appoints the first holder via `freeze_authority_transfer` or a nomination, the same paths that repopulate a renounced slot. There is no initialisation ordering to get right because there is no initializer. The admin slot next door is bootstrapped by marking that same instruction with `#[admin_initialize]`, the caller becomes admin in the transaction that creates the account.\n- **Slot markers keep the layout honest.** `#[admin_slot]` and `#[freeze_slot]` each derive an offset const and a layout test, and the build fails if a marker position and its `offset = ...` declaration ever disagree, for example after a field is added above a slot.\n- **One account per transaction.** When admin and freeze share the embedding account, management instructions that read both carry the shared account once. `freeze_authority_renounce` drops from 3 accounts to 2.\n- **Splice-only writes.** Freeze operations write only the 65 byte window (the 64 byte slot plus the frozen flag), your neighbouring fields survive every toggle and transfer.\n- **Offsets never appear in a transaction.** They compile into the program as literals, and the IDL carries no offset arguments.\n\nThe library repository ships `freeze-authority-sample-embedded` with the full layout, adjacent-window tests, and a committed dry-run walkthrough.\n\n## Verify your integration\n\nAfter building your program, check that the freeze instructions appear in the IDL:\n\n```bash\n# From a `spel init` project root, drop the path entirely and the guest is auto-detected.\nspel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'\n```\n\nThe `spel` binary must be built from the same framework revision your `Cargo.toml` pins. A CLI built without the extension scanner omits every extension instruction from this output without reporting an error. The check is also not a substitute for a build, `generate-idl` reads your source rather than compiling it, so run `cargo check` first.\n\nExpected output includes:\n\n```\n\"admin_initialize\"\n\"admin_transfer\"\n\"admin_renounce\"\n\"admin_nominate\"\n\"admin_accept\"\n\"freeze_initialize\"\n\"freeze_authority_transfer\"\n\"freeze_authority_renounce\"\n\"freeze_program\"\n\"freeze_program_release\"\n\"freeze_account\"\n\"freeze_account_release\"\n\"freeze_authority_nominate\"\n\"freeze_authority_accept\"\n```\n\nPlus your own instructions. In embedded mode neither `admin_initialize` nor `freeze_initialize` appears, and the config accounts in every instruction are your own embedding account instead of the dedicated PDAs. On the pins above the surface cannot go missing quietly: a marker that matches no discoverable extension is a hard compile error naming the marker, and from `freeze-authority` v0.1.3 a marker written above `#[lez_program]` is rejected on sight rather than skipped. When you hit one of those errors, the most common causes are:\n\n- `freeze-authority` not declared as a path or git dependency in your `Cargo.toml`.\n- `admin-authority` missing (freeze-authority hard-depends on it).\n- `#[freeze_authority]` placed above `#[lez_program]` rather than below it. An imported marker names the misplacement, an unimported one fails with `cannot find attribute freeze_authority in this scope`.\n- Cached macro expansion, run `cargo clean -p <your-crate>` and rebuild.\n\n## Security notes\n\n- **Initialisation order matters.** `freeze_initialize` requires admin signature and an initialised `admin_config`. Send `admin_initialize` right after deployment and `freeze_initialize` as soon as it lands. Both come from the admin key, so the second waits until the first has landed, see [Build and deploy the program](https://docs.logos.co/lez/extensions/admin-authority.md#build-and-deploy-the-program).\n- **Renounce is recoverable (unlike admin).** Vacating the freeze authority slot is reversible by the admin via `freeze_authority_transfer` or a nomination. Plan accordingly if your operational model assumes the role is permanent, only renouncing admin first locks it down.\n- **Exempt is shallow.** A `#[freeze_exempt]` consumer function that uses `chained_call` to invoke a gated function still hits the gated function's check. Frozen-state behaviour of chained calls is determined by the called function's exemption status, not the caller's.\n- **Auto mode covers all dispatched instructions.** Including admin operations? No, admin-authority's five management instructions are exempt by an explicit entry in freeze-authority's metadata. Admin can still transfer or renounce while the program is frozen. This is by design to avoid deadlock from a lost admin key during freeze.\n- **Per-account PDAs persist.** Per-account freeze state writes a PDA per target. Once initialised, the PDA exists for the program's lifetime (LEZ has no close primitive). Toggling release writes `is_frozen = false`; the PDA itself stays. No rent applies in LEZ.\n\n## Reference\n\nSource: [github.com/mmlado/spel-freeze-authority](https://github.com/mmlado/spel-freeze-authority). The companion repository contains the authority lifecycle state diagram, ADRs for design decisions (including ADR-0007 on renounce semantics and ADR-0008 on per-account encoding), the LEZ rent investigation, and reference sample programs demonstrating both auto and manual modes end-to-end.\n"},{"id":"lez/extensions/build-a-spel-extension-library","title":"Build a SPEL extension library","summary":"This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.","section":"lez","doc_type":"procedure","url":"https://docs.logos.co/lez/extensions/build-a-spel-extension-library","markdown_url":"https://docs.logos.co/lez/extensions/build-a-spel-extension-library.md","permalink":"/lez/extensions/build-a-spel-extension-library","source":"https://github.com/logos-co/logos-docs/blob/main/docs/lez/extensions/build-a-spel-extension-library.md","headings":[{"depth":2,"text":"What an extension provides"},{"depth":2,"text":"Layout"},{"depth":2,"text":"The discovery metadata"},{"depth":2,"text":"Define the runtime library"},{"depth":2,"text":"Define the proc-macro sub-crate"},{"depth":2,"text":"Per-instruction gate attributes (optional)"},{"depth":2,"text":"Auto-wrap every instruction (optional)"},{"depth":2,"text":"Embedded mode (optional)"},{"depth":3,"text":"Attribute-order convention in library source"},{"depth":2,"text":"Composing with another extension (hard dependency)"},{"depth":2,"text":"Consumer integration"},{"depth":2,"text":"Multiple extensions on one program"},{"depth":2,"text":"Verifying your extension"},{"depth":2,"text":"Why this design"}],"markdown":"# Build a SPEL extension library\n\n> **Warning**\n>\n> This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. This content is still being completed and verified.\n>\n> The dependency snippets pin the framework at the upstream `v0.7.0` tag, the first release that carries the extension mechanism ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)).\n\nSPEL extension libraries ship reusable on-chain primitives, access control, freeze switches, multisig, etc., that consuming [programs](https://docs.logos.co/get-started/glossary.md#program) adopt with a single attribute. This guide is for library authors. App developers consuming an existing extension should follow that extension's own integration guide instead.\n\n## What an extension provides\n\nAn extension is a normal Rust crate that:\n\n1. Defines one or more `#[instruction]` functions that consumers can call from the SPEL CLI / wallets.\n1. Declares a marker attribute name in its `Cargo.toml` so consumers can opt in.\n1. Optionally ships per-instruction gate attributes (like `#[require_admin]`) that consumers apply to their own instructions.\n\nWhen a consumer puts the marker attribute on a `#[lez_program]` module, the framework discovers the extension via Cargo metadata, scans the library's `src/lib.rs` for `#[instruction]` functions, and merges them into the consumer's dispatcher and IDL automatically. No framework changes are needed per extension.\n\n## Layout\n\nA SPEL extension is typically two crates plus an optional sample:\n\n```\nmy-extension/\n├── my-extension/             # runtime library: types, instruction fns, metadata\n│   ├── Cargo.toml\n│   └── src/lib.rs\n├── my-extension-macros/      # proc-macro sub-crate: marker + gate attributes\n│   ├── Cargo.toml\n│   └── src/lib.rs\n└── my-extension-sample/      # reference SPEL program (optional but recommended)\n    └── ...\n```\n\nThe split exists because Rust requires proc-macro attributes to live in a `proc-macro = true` crate that cannot also export non-macro items. The runtime library re-exports the macros so consumers only declare one dependency.\n\n## The discovery metadata\n\nIn `my-extension/Cargo.toml`:\n\n```toml\n[package]\nname = \"my-extension\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[package.metadata.spel]\nextension_attr = \"my_extension\"\n\n[dependencies]\nborsh = { version = \"1\", features = [\"derive\"] }\nspel-framework = { git = \"https://github.com/logos-co/spel\", tag = \"v0.7.0\" }\nmy-extension-macros = { path = \"../my-extension-macros\" }\n```\n\n- `extension_attr` is the attribute name consumers put on their `#[lez_program]` module to opt in. By convention, match it to your crate name (with `_` not `-`).\n\nPer-instruction gate attributes your library defines (for example, `#[require_admin]` from `admin-authority`) need no metadata for the check itself: they are ordinary proc-macros that re-expand on the emitted handler and consume themselves, so the framework leaves them alone. If your gate needs specific [account](https://docs.logos.co/get-started/glossary.md#account) parameters on every gated instruction, you can declare those in an optional inject block so consumers do not have to write them out (see the gate attribute section below).\n\n## Define the runtime library\n\n`my-extension/src/lib.rs`:\n\n```rust\nuse borsh::{BorshDeserialize, BorshSerialize};\nuse spel_framework::prelude::*;\n\nextern crate self as my_extension;\n\npub use my_extension_macros::{instruction, my_extension};\n\n#[derive(BorshSerialize, BorshDeserialize, Clone)]\npub struct MyState {\n    pub value: u64,\n}\n\n#[instruction]\npub fn extension_action(\n    #[account(mut, pda = literal(\"my_state\"))] mut my_state: AccountWithMetadata,\n    #[account(signer)] caller: AccountWithMetadata,\n    new_value: u64,\n) -> SpelResult {\n    todo!(\"read state, mutate, write\")\n}\n```\n\nThree things to note:\n\n- `extern crate self as my_extension;`, lets the library reference its own types via the absolute path `::my_extension::MyState`. The framework emits cross-crate calls into the consumer's binary using that path, so the path needs to resolve both in the library's own compile and at the consumer's compile.\n- `pub use my_extension_macros::{instruction, my_extension};`, re-exports the marker attribute and the no-op `#[instruction]` shim so consumers (and the library's own `lib.rs`) can use them without importing the macros crate directly.\n- `#[account(...)]` attributes on parameters, these are framework helper attributes that describe [PDA](https://docs.logos.co/get-started/glossary.md#pda) seeds, signer requirements, etc. The library's own `#[instruction]` shim strips them at the library's compile so rustc accepts the source; the framework reads them during the path-dependency scan.\n- Name the state parameter after the inject role you declare for it (`my_state` here). Injection reuse, wrap stamping, and embedded retargeting resolve your accounts by role name, a differently named parameter breaks embedded mode with an argument-count error at the consumer's compile.\n- When you write the real body, post-states are the inner `account` values (`vec![my_state.account, caller.account]`). If the instruction claims any account, every entry in that `vec` becomes an `(account, AutoClaim)` tuple instead, with `AutoClaim::None` for the ones it does not claim, the way `admin-authority`'s `admin_initialize` returns `vec![(config.account, AutoClaim::Claimed(..)), (caller.account, AutoClaim::None)]`. The `vec` is homogeneous either way, mixing a bare `account` with a tuple fails to compile with `expected Account, found (Account, AutoClaim)`. Consumer handlers return the `AccountWithMetadata` wrappers, library handlers do not. The reference samples show both patterns.\n\n## Define the proc-macro sub-crate\n\n`my-extension-macros/Cargo.toml`:\n\n```toml\n[package]\nname = \"my-extension-macros\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nproc-macro = true\n\n[dependencies]\nproc-macro2 = \"1\"\nquote = \"1\"\nsyn = { version = \"2\", features = [\"full\"] }\n```\n\n`my-extension-macros/src/lib.rs`:\n\n```rust\nuse proc_macro::TokenStream;\nuse quote::quote;\nuse syn::{parse_macro_input, FnArg, ItemFn};\n\n/// Marker attribute. Near pass-through: the framework detects it on a\n/// #[lez_program] module by name and triggers the path-dependency scan for\n/// `my-extension`. The one thing it must reject is its own misplacement.\n#[proc_macro_attribute]\npub fn my_extension(_attr: TokenStream, item: TokenStream) -> TokenStream {\n    if let Ok(module) = syn::parse::<syn::ItemMod>(item.clone()) {\n        if module\n            .attrs\n            .iter()\n            .any(|a| a.path().is_ident(\"lez_program\"))\n        {\n            return syn::Error::new_spanned(\n                &module.ident,\n                \"#[my_extension] must come after #[lez_program]: a marker above \\\n                 it expands first and is invisible to the framework\",\n            )\n            .to_compile_error()\n            .into();\n        }\n    }\n    item\n}\n\n/// No-op `#[instruction]` for the library's own source. Strips `#[account(...)]`\n/// helper attrs from params so rustc accepts the library's compile.\n#[proc_macro_attribute]\npub fn instruction(_attr: TokenStream, item: TokenStream) -> TokenStream {\n    let mut func = parse_macro_input!(item as ItemFn);\n    for arg in &mut func.sig.inputs {\n        if let FnArg::Typed(pt) = arg {\n            pt.attrs.retain(|a| !a.path().is_ident(\"account\"));\n        }\n    }\n    quote!(#func).into()\n}\n```\n\nThe framework treats `#[my_extension]` as a marker by attribute name only, it does not invoke the library's macro to discover anything. Past the misplacement check the expansion is irrelevant, so pass-through is right for everything else. Guard the misplacement in the macro rather than leaving it to the consumer: a marker above `#[lez_program]` is consumed before the framework ever sees it, so a consumer who imported your marker's name gets a green build with your whole surface missing and only `spel generate-idl` reports it. A consumer who did not import it gets rustc's ``cannot find attribute `my_extension` in this scope`` instead, which says nothing about attribute order, so neither reader is served by leaving the check out.\n\n## Per-instruction gate attributes (optional)\n\nIf your extension provides a check that consumers apply to specific instructions (analogous to `#[require_admin]` in `admin-authority`), add another `#[proc_macro_attribute]` to the macros crate. The pattern is body injection by re-expansion: `#[lez_program]` leaves your attribute on the handler it emits, so your macro runs after the framework, prepends its check to the handler body, and removes itself by returning the function without the attribute.\n\n```rust\nuse syn::{parse_quote, ItemFn};\n\n#[proc_macro_attribute]\npub fn require_my_gate(_attr: TokenStream, item: TokenStream) -> TokenStream {\n    let mut func: ItemFn = match syn::parse(item) {\n        Ok(f) => f,\n        Err(e) => return e.to_compile_error().into(),\n    };\n\n    // Prepend the runtime check. This minimal form hardcodes the conventional\n    // parameter names; a shippable gate parses `_attr` for them instead, see\n    // the kwarg contract below.\n    let prologue: syn::Stmt = parse_quote! {{\n        let __state = ::my_extension::MyState::from_account(&my_state)?;\n        __state.assert_allowed(&caller)?;\n    }};\n    func.block.stmts.insert(0, prologue);\n\n    quote::quote!(#func).into()\n}\n```\n\nThat snippet ignores its attribute args to keep the shape visible, which is enough only while every gated instruction happens to use the conventional names. Parsing them is not optional for a gate you ship, see the kwarg contract below. The `from_account` and `assert_allowed` helpers the prologue calls are yours to write on `MyState`, the framework provides neither. Re-export the gate from the runtime library next to the marker (`pub use my_extension_macros::require_my_gate;`) so consumers import everything from one crate.\n\nNever read or strip `#[account(...)]` attributes in a gate macro. That attribute belongs to the framework, which reads it for validation and the IDL. Your gate should only reference parameter names, taken from its own attribute args with sensible defaults.\n\n**The kwarg contract.** Your gate's attribute keys must be exactly the inject-account names you declare in metadata (`#[require_my_gate(my_state = their_cfg, caller = owner)]`). The framework's auto-wrap and gate stamping emit every kwarg with the resolved parameter name, so your macro receives the framework's naming decisions instead of guessing from convention. Ship an alignment self-test so the two cannot drift: read your own metadata with `spel_framework_core::extension::read_inject_specs(Path::new(env!(\"CARGO_MANIFEST_DIR\")))`, which returns `Result<Vec<InjectSpec>, String>`, and assert the declared account names equal the kwarg set a probe function hands your gate. A name the macro rejects fails the probe's compile, a metadata rename fails the runtime assert. The `extension` module sits behind `spel-framework-core`'s non-default `idl-gen` feature and is not reachable through `spel-framework`, so the test needs its own dev-dependency:\n\n```toml\n[dev-dependencies]\nspel-framework-core = { git = \"https://github.com/logos-co/spel\", tag = \"v0.7.0\", features = [\"idl-gen\"] }\n```\n\nTo spare consumers declaring your gate's account parameters on every gated instruction, declare them in your `Cargo.toml`:\n\n```toml\n[[package.metadata.spel.inject]]\nwrapper = \"require_my_gate\"\n\n  [[package.metadata.spel.inject.account]]\n  name = \"my_state\"\n  seed = { const = \"my_state\" }\n\n  [[package.metadata.spel.inject.account]]\n  name = \"caller\"\n  signer = true\n```\n\nAny consumer instruction carrying `#[require_my_gate]` gets the listed parameters synthesised at expansion time unless it already declares them (skip-if-declared). The injected parameters are exactly what the explicit declaration would have been, land after a leading `ProgramContext` in the block's declaration order, and appear in the IDL like any declared account. Compound PDA seeds work too: `seed = [{ const = \"frozen\" }, { account = \"caller\" }]` derives from a literal plus another account's id.\n\n## Auto-wrap every instruction (optional)\n\nIf your extension is a circuit-breaker primitive (an emergency stop, a re-entrancy guard, a global rate limit), you may want EVERY consumer instruction gated, not just the ones the consumer remembered to annotate. The framework supports this via a `wrap_instructions` metadata field that activates a module-level wrap hook:\n\n```toml\n[package.metadata.spel.wrap_instructions]\nwrapper = \"my_extension::require_my_gate\"\nskip = \"manual\"\nself_exempt_marker = \"my_extension_exempt\"\nexempt = [\n  \"admin_authority::admin_initialize\",\n  \"admin_authority::admin_transfer\",\n  \"admin_authority::admin_renounce\",\n]\n```\n\n- `wrapper`: a qualified path to the per-instruction attribute the framework prepends onto each non-exempt dispatched function, including other extensions' discovered instructions. Reuses the same `#[require_my_gate]` attribute consumers apply by hand in manual mode, one proc-macro, two callers.\n- `skip`: the arg literal on your `#[my_extension]` marker that DISABLES auto-wrap. With `skip = \"manual\"`, `#[my_extension(manual)]` opts out of the wrap and `#[my_extension]` (bare) opts in.\n- `self_exempt_marker`: an attribute name the framework recognises as \"skip this function from wrap.\" Add another pass-through proc-macro of that name to your macros crate; consumers carry it on any instruction they want to remain callable while gated.\n- `exempt`: a list of cross-crate dispatched instructions to skip unconditionally. Use this when composing with another extension whose ops must stay operable even when your wrap is active. (Self-exemptions for your own instructions go on the function via `self_exempt_marker` instead.)\n\nWhen the consumer puts `#[my_extension]` on their `#[lez_program]` mod, the framework walks the dispatcher table and prepends `#[require_my_gate]` to every function that is not in `exempt` and does not carry `#[my_extension_exempt]`. Consumers write normal code, the gate arrives with the wrap. Injection and wrapping compose: a wrapped instruction gets your gate's parameters injected like an annotated one.\n\nThe wrap covers your own extension's instructions too, and those dispatch cross-crate, so the dispatcher cannot add a parameter to your library's function. A wrapped own instruction whose parameter list does not cover your inject roles, by role name or matching seed, fails the consumer's compile with an argument-count error on the dispatch call. Own instructions that lack your gate accounts, or that must stay callable while your gate rejects, carry your own `self_exempt_marker` attribute in the library source, the way freeze-authority's release and transfer ops carry `#[freeze_exempt]`.\n\nThe hook is opt-in, omit `wrap_instructions` from your metadata and the framework leaves all instructions alone. This is the right choice for most extensions (pure data primitives, single-instruction gates, etc.).\n\n## Embedded mode (optional)\n\nAn extension whose per-program state is one fixed-size slot can let consumers embed that slot inside one of their own accounts instead of a dedicated PDA. The consumer declares it on your marker, role name plus byte offset:\n\n```rust\n#[my_extension(my_state = config, offset = 32)]\n```\n\nTo support this as an author: ship windowed state accessors that splice only your slot's byte window (`decode_at`, `write_to_at`, `bootstrap_at` and friends), give the affected instruction functions a trailing `offset: usize` parameter, keep the state parameter named after its inject role (retargeting resolves it by that name), and declare the offset as a bound arg so the framework fills it at the dispatch call site as a compile-time literal. Bound args must be the trailing parameters of the function, in the same order as their metadata blocks. Any other position is a hard error at discovery naming the function, because the framework always appends the literals last:\n\n```toml\n[package.metadata.spel.embedded]\nskip = [\"my_extension_initialize\"]\nstate_type = \"my_extension::MyConfig\"\n\n[[package.metadata.spel.bound_args]]\narg = \"offset\"\nfrom = \"offset\"\ndefault = 0\n```\n\n`embedded.skip` names instructions not emitted in embedded mode (typically your initializer, the consumer's own account creation replaces it). `state_type` is mandatory for embedded mode and names the Rust type that lives in your window. The framework reads the window's size through it and emits a compile-time assert per pair of extensions embedding into the same consumer account, so genuinely overlapping windows refuse to compile instead of silently corrupting each other. Discovery fails closed when an embedded extension omits it. Bound args are stripped from the IDL and the transaction entirely, a caller can never supply an offset, and dedicated mode is the degenerate case offset 0 through the declared default. `from` also accepts a peer marker's kwarg (`from = \"admin_authority::offset\"`) so an extension can read state a peer embedded, without depending on the peer's crate. A missing marker or kwarg without a declared default is a hard compile error, never a silent zero.\n\n**Slot field markers.** The framework derives a marker name from your role, the role name minus a `_config` suffix plus `_slot` (role `admin_config` gives `#[admin_slot]`, role `my_state` gives `#[my_state_slot]`). A consumer who puts that marker on the embedding field of an `#[account_type]` struct gets a derived `<MARKER>_OFFSET` const, an emitted layout test, and a compile-time assert that the derived offset equals the `offset = ...` declared on your marker, so a field added above the slot fails the build instead of silently moving the window. Adoption is optional (no marker, no check), and two structs carrying the same marker is a compile error. Nothing to implement on your side, the mechanism ships with `#[account_type]`, but document the marker name your role produces.\n\n**Born-initialized slots.** If your slot must never exist uninitialised (the way an admin slot without a holder is a takeover window), ship a bootstrap attribute consumers put on their own account-creating instruction (the way `admin-authority` ships `#[admin_initialize]`). Implement it as a proc macro that injects your `bootstrap_at` call into the handler body, and declare it as an inject wrapper in metadata so your role parameters synthesise on the marked instruction like they do on gates. Make it embedded-mode only and let the framework stamp the location kwargs, and reject instructions whose embedding account is not `init`, a bootstrap against an existing account is a takeover. A slot that can start empty (the way freeze starts vacant and the admin appoints the first holder via transfer) needs no bootstrap attribute at all.\n\nWhen two extensions embed into the same consumer account at distinct offsets, the framework merges the duplicated account into one transaction account (listed once in the IDL with unioned constraints, cloned into each position of the call) and your instruction must emit exactly one post-state per unique account id. Same account at the same offset is a compile error.\n\n### Attribute-order convention in library source\n\nIf a per-instruction gate attribute reads the `#[account(...)]` attributes on parameters to validate their shape, the order of attributes on the library's own `#[instruction]` functions matters:\n\n```rust\n#[require_my_gate]   // runs first, sees params with #[account(...)] intact\n#[instruction]       // shim runs second, strips #[account(...)] for rustc\npub fn gated_op(/* ... */) -> SpelResult { /* ... */ }\n```\n\nRust expands attribute macros top-down. The library's `#[instruction]` shim strips `#[account(...)]` from parameters. A gate placed below `#[instruction]` runs after the strip, no PDA or signer attributes are left for its shape check, and it emits a confusing error.\n\nA body-inject gate that references parameters by name only, the way `#[require_admin]` and the gate in this guide do, is order-independent. The shim strips attributes, never parameters, so the names it references survive in either position. The rule only applies to gates that read parameter attributes, inside libraries that re-export the shim. In consumer code the stripper is `#[lez_program]` itself: the module rewrite removes `#[instruction]` and `#[account(...)]` from every instruction before any function-level attribute expands, so gate order never matters there, and a gate never sees account attributes at all.\n\n## Composing with another extension (hard dependency)\n\nSome extensions naturally build on others. `freeze-authority` depends on `admin-authority`, its freeze-authority slot is governed by admin signatures. When your extension does this:\n\n1. **Declare a normal Cargo dependency** on the other extension in your `Cargo.toml`, path or git. `freeze-authority` uses a git dependency on `admin-authority` pinned to its `v0.2.0` tag. Consumers get both extensions in their dependency graph automatically.\n1. **Add both markers to the consumer's mod.** Consumers write `#[admin_authority] #[my_extension]` on their `#[lez_program]` mod. Each marker triggers its own discovery.\n1. **Import the gate attributes you compose with.** For example, `use admin_authority::require_admin;` in your library source, then `#[require_admin]` on instructions that should require an admin signature (like an initialisation that creates your config PDA).\n1. **List the other extension's exempt-while-wrapped instructions** in your `wrap_instructions.exempt` if applicable. freeze-authority lists admin-authority's five management instructions so they stay callable while the program is frozen.\n\nThe framework deduplicates path-dependency directories, so admin-authority is scanned once even if both your extension and the consumer name it as a path dependency.\n\n## Consumer integration\n\nA consumer adds your extension to their `Cargo.toml`:\n\n```toml\n[dependencies]\nmy-extension = { git = \"https://github.com/you/my-extension\" }\nspel-framework = { git = \"https://github.com/logos-co/spel\", tag = \"v0.7.0\" }\nnssa_core = { git = \"https://github.com/logos-blockchain/logos-execution-zone.git\", tag = \"v0.2.4\", package = \"lee_core\" }\nserde = { version = \"1\", features = [\"derive\"] }\n```\n\n`nssa_core` and `serde` are named directly by `#[lez_program]`'s expansion, so the consumer must declare both even though their own source never mentions either.\n\nThe `spel-framework` pin must carry the extension scanner, which upstream `logos-co/spel` does from `v0.7.0` ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)), and it must be the same reference your library pins, spelt the same way. Cargo keys git sources by their reference, so a branch, or a `rev` pointing at the same commit, does not unify with a tag pin.\n\nPath, git, and registry dependencies are all discoverable. Discovery is restricted to the consumer's direct dependencies, a transitive crate can never contribute instructions by claiming a matching `extension_attr`, and the generated call paths use your `[package].name`, never a directory name.\n\nThen puts the marker on their `#[lez_program]` module:\n\n```rust\nuse spel_framework::prelude::*;\n\n#[lez_program]\n#[my_extension]\nmod my_program {\n    #[instruction]\n    pub fn my_user_instr(...) -> SpelResult { ... }\n}\n```\n\nThe marker is matched by attribute name only, nothing is imported for it. `use my_extension::my_extension;` would only earn an unused import warning. Gate attributes and types your extension expects consumers to name in code are real imports, document those in your library's README.\n\n`#[lez_program]` must be the outermost attribute on the module. Markers sit below it and are consumed during expansion. Only inert markers may sit there: the module rebuild does not re-emit inner attributes, so a real attribute macro placed below `#[lez_program]` is dropped silently and never runs.\n\nAfter compilation, the consumer's binary contains your extension's instructions in its `Instruction` enum, dispatcher, and `PROGRAM_IDL_JSON` const. `spel generate-idl` shows them too. The extension's source is never copied into the consumer's module; calls dispatch directly to your library via `::my_extension::extension_action(...)`.\n\n## Multiple extensions on one program\n\nConsumers can stack extensions without coordination between library authors:\n\n```rust\n#[lez_program]\n#[admin_authority]\n#[my_extension]\nmod my_program { ... }\n```\n\nEach extension is discovered independently by its own `extension_attr`. Each contributes its own instructions to the dispatcher, and each gate attribute re-expands on its own gated handlers without touching the others.\n\nMarker order is the cross-extension ABI: the first marker's instructions and injected parameters come first in the dispatcher, the IDL, and the account order. When two extensions inject the same parameter name with identical constraints they share one account, conflicting constraints are a compile error naming both extensions. Duplicate instruction names between extensions (or an extension and a consumer function) are a compile error naming both sources. Two extensions can even embed into the same consumer account at distinct offsets, see embedded mode above.\n\n## Verifying your extension\n\nBuild a small sample program that consumes your extension. Then:\n\n```bash\nspel generate-idl path/to/sample/src/main.rs\n```\n\nThe IDL should contain your extension's instructions alongside the consumer's own. The `spel` binary must itself be built from a scanner-carrying framework revision, a CLI without the scanner omits every extension instruction from this output without reporting an error. The check is also not a substitute for a build, `generate-idl` reads the source rather than compiling it, so a sample that does not compile still produces a complete-looking IDL.\n\nOn a framework build that carries the extension scanner, a marker that matches no discoverable extension is a hard compile error naming the marker, regardless of why it did not match, so a broken setup refuses loudly instead of building a program silently missing its extension surface. The fail-closed behaviour is a property of the framework revision, not of the mechanism: on a build without the scanner the marker is ignored and the program compiles without your extension. An author debugging a missing surface should check the framework pin before the metadata. When you hit the hard error, or a green build with your surface missing, the most common causes are:\n\n- `[package.metadata.spel.extension_attr]` not declared, or value does not match the attribute name the consumer wrote.\n- The library is a transitive dependency rather than a direct one. Only the consumer's own `[dependencies]` are scanned, by design.\n- Your marker written above `#[lez_program]` rather than below it. This one does not reach the hard error unless your marker macro rejects it: the attribute is consumed before the framework sees it, so a consumer who imported the marker's name gets a green build with your surface absent, and `spel generate-idl` is the only thing that reports it, failing with `Malformed extension metadata: 'extension marker #[my_extension] is above #[lez_program]…'` and a non-zero exit. A consumer who never imported the name gets rustc's ``cannot find attribute `my_extension` in this scope`` at the build instead.\n- Cached macro expansion, try `cargo clean -p <sample-crate>` and rebuild. Cargo doesn't know proc-macros read external `Cargo.toml` files, so metadata changes don't always invalidate the cache.\n\nMalformed `[package.metadata.spel]` is a hard compile error too, never a silent skip. When dependency resolution itself degrades (for example `cargo metadata` failing in a constrained environment) but every marker still matched a path dependency, the degradation stays a warning and the build continues.\n\n## Why this design\n\nEarlier iterations of SPEL handled the same use case by hardcoding extension support in `spel-framework-macros`, adding an `#[admin_authority]` macro to the framework itself with templates baked in. That approach required a framework PR per extension and coupled every extension to the framework's release cycle. The metadata-driven scanner replaces it: framework knows nothing specific about any extension, libraries ship independently, and the same mechanism scales to any number of extension crates.\n"},{"id":"core/build-modules/start-a-logos-module-from-the-cli","title":"Start a Logos module from the CLI","summary":"Explore how to load and call a Logos module from the command line using `logosctl`.","section":"core","doc_type":"procedure","url":"https://docs.logos.co/core/build-modules/start-a-logos-module-from-the-cli","markdown_url":"https://docs.logos.co/core/build-modules/start-a-logos-module-from-the-cli.md","permalink":"/core/build-modules/start-a-logos-module-from-the-cli","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/build-modules/start-a-logos-module-from-the-cli.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Install the module"},{"depth":2,"text":"Call module methods"},{"depth":2,"text":"Troubleshooting `logosctl` module startup"},{"depth":3,"text":"The platform key in `manifest.json` does not match"},{"depth":3,"text":"The install is refused without confirmation"},{"depth":3,"text":"The module is not loaded after a restart"}],"markdown":"# Start a Logos module from the CLI\n\n#### Explore how to load and call a Logos module from the command line using `logosctl`.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis guide covers how to build and install a Logos [module](https://docs.logos.co/get-started/glossary.md#module), start the `logosctl` daemon, and call module methods from the command line. It is intended for users who want to run a module locally, or developers who want to test a module they are building. You create the module from the `logos-module-builder` template, so there is no code to write. By the end you will have a running `logosctl` instance that loads the template's `minimal` module and returns results from its `greet` and `getStatus` methods.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux x86_64 or aarch64\n>    - macOS arm64 (Apple Silicon)\n> - Git\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n> - Basic familiarity with the command line and Nix concepts\n\n## What to expect\n\n- You can build a module from the `logos-module-builder` template, load it into a running `logosctl` daemon and call its methods.\n- You can list the methods and events a module exposes, with the descriptions its author wrote.\n- You have a `logosctl` session with a `modules` directory that future modules install into.\n\n## Install the module\n\n`logosctl` expects each module in its own subdirectory containing a `manifest.json`. `logosctl package install` handles this layout automatically when given an [LGX](https://docs.logos.co/get-started/glossary.md#lgx) package, and unpacks it into the current [session's](https://github.com/logos-co/logos-logoscore-cli/blob/master/docs/logosctl.md#sessions) own `modules/` directory.\n\n1. Create a module from the `logos-module-builder` template and build its LGX [package](https://docs.logos.co/get-started/glossary.md#package):\n\n   ```bash\n   mkdir hello-module\n   cd hello-module\n   nix flake init -t github:logos-co/logos-module-builder\n   git init && git add -A\n\n   nix build '.#lgx-portable'\n\n   cd ..\n   ```\n\n   - The template is a complete module named `minimal`: `src/minimal_impl.h` declares its methods, and the build generates the rest.\n   - Nix only sees files that Git tracks, so `git add` comes before the build.\n   - Build `lgx-portable`, not `lgx`: the `lgx` output is a development build (variant `darwin-arm64-dev` on Apple Silicon, for example) that `logosctl package install` rejects. See [Troubleshooting](#troubleshooting-logosctl-module-startup).\n\n1. Start the daemon, detached so this terminal stays free:\n\n   ```bash\n   logosctl daemon start --detach\n   ```\n\n   - The detached command returns after the Logos node is ready to accept commands.\n\n1. Check the status of `logosctl`:\n\n   ```bash\n   logosctl daemon status\n   ```\n\n1. Install the LGX package into the current session:\n\n   ```bash\n   logosctl package install --file ./hello-module/result/*.lgx\n   ```\n\n   `logosctl` lists the change and asks you to confirm. Pass `-y` to skip the prompt. Without a terminal, for example in a script, `-y` is required.\n\n   This unpacks into the `logosctl` session's `modules/` directory:\n\n   ```\n   modules/minimal/\n   ├── minimal_plugin.dylib     # (or .so on Linux)\n   ├── assets/lidl/minimal.lidl # The module's interface, in LIDL\n   ├── manifest.json            # Package manifest\n   ├── variant                  # Platform variant identifier\n   └── ...                      # Runtime libraries bundled by the portable build\n   ```\n\n1. Confirm the module was installed correctly:\n\n   ```bash\n   logosctl package ls\n   ```\n\n   `minimal` is listed with the source `user`, next to the packages embedded in `logosctl`.\n\n## Call module methods\n\nWith the module installed, load the module with `logosctl` and call its methods.\n\n1. Load the module and confirm that it was loaded:\n\n   ```bash\n   logosctl module load minimal\n\n   logosctl module ls --loaded\n   ```\n\n1. Call `greet` with a name:\n\n   ```bash\n   logosctl call minimal greet World\n   ```\n\n   ```\n   Hello, World! Greetings from the minimal module.\n   ```\n\n1. Call `getStatus`, which takes no arguments:\n\n   ```bash\n   logosctl call minimal getStatus\n   ```\n\n   ```\n   Minimal module is running.\n   ```\n\n1. Inspect the methods and events that `minimal` exposes:\n\n   ```bash\n   logosctl module show minimal\n   ```\n\n   ```\n   Methods:\n     greet(name: tstr) -> tstr\n         Returns a greeting and announces it as a typed `greeted` event.\n     getStatus() -> tstr\n         Returns a short status string.\n     ...\n\n   Events:\n     greeted(greeting: tstr)\n         Emitted by greet() with the greeting it produced. Other modules\n         subscribe with `modules().minimal.onGreeted(...)`.\n   ```\n\n   - The descriptions are the `///` comments in `src/minimal_impl.h`.\n   - Every module also has `name()`, `version()` and `lidl()`, which the output lists after `getStatus`.\n\n1. Stop the daemon:\n\n   ```bash\n   logosctl daemon stop\n   ```\n\n## Troubleshooting `logosctl` module startup\n\n### The platform key in `manifest.json` does not match\n\n`logosctl package install` fails with an error such as:\n\n```\nError: install failed at step 'install': Package does not contain variant for platform: darwin-arm64 (package provides: darwin-arm64-dev)\n```\n\nAn LGX package holds one variant per platform, and the released `logosctl` installs only the portable variant for the platform it runs on. A `-dev` variant comes from the `lgx` output, so build `lgx-portable` instead. A variant for another platform means the package was built there; rebuild it on the target platform and reinstall.\n\n### The install is refused without confirmation\n\n`logosctl package install` reports `Refusing to proceed without confirmation. Pass -y to continue.` when no terminal is available to confirm in. Pass `-y`.\n\n### The module is not loaded after a restart\n\nRestarting the daemon does not reload modules, so `logosctl call` reports `Module 'minimal' is not loaded`. Load it again with `logosctl module load minimal`.\n"},{"id":"core/build-modules/build-and-run-a-logos-core-module","title":"Build and run a Logos core module","summary":"Scaffold, build, package, and test a core module on Logos.","section":"core","doc_type":"procedure","url":"https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module","markdown_url":"https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module.md","permalink":"/core/build-modules/build-and-run-a-logos-core-module","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/build-modules/build-and-run-a-logos-core-module.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Scaffold the module project"},{"depth":2,"text":"Step 2: Adapt the template for your module"},{"depth":2,"text":"Step 3: Build the module"},{"depth":2,"text":"Step 4: Inspect your module"},{"depth":2,"text":"Step 5: Package the module"},{"depth":3,"text":"Build a Windows package"},{"depth":2,"text":"Step 6: Install the module"},{"depth":3,"text":"Install a locally built `.lgx` package"},{"depth":3,"text":"Install a package by name from the catalogue"},{"depth":2,"text":"Step 7: Run the module"},{"depth":3,"text":"Run with `logosctl`"},{"depth":3,"text":"Run with Logos Basecamp"},{"depth":2,"text":"Troubleshooting"},{"depth":3,"text":"Known constraints"},{"depth":3,"text":"Nix reports an \"experimental features\" error"},{"depth":3,"text":"The build fails with `Failed to open header file` or `unknown type name`"},{"depth":3,"text":"The build reports that the module declares no `interface`"},{"depth":3,"text":"Module not discovered by Logos Basecamp"},{"depth":3,"text":"Module not discovered by `logosctl`"},{"depth":3,"text":"`logosctl package install` fails"},{"depth":3,"text":"LGX variant mismatch"},{"depth":3,"text":"`nix build .#lib` does nothing or fails silently"},{"depth":3,"text":"First build is slow"}],"markdown":"# Build and run a Logos core module\n\n#### Scaffold, build, package, and test a core module on Logos.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nLogos is a modular application framework built on Qt 6. Applications are composed of dynamically loaded modules that provide features and functionality. Logos [core modules](https://docs.logos.co/get-started/glossary.md#core-module) are non-UI modules that provide backend functionality, loaded by a Logos runtime such as `logosctl` or Logos Basecamp.\n\nYou write a core module as one plain C++ class: its public methods are the module's API, and `logos-module-builder` generates the Qt plugin around it. This procedure scaffolds a module from the builder's template, builds and inspects it, packages it, and runs it with `logosctl` and with Logos Basecamp.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux x86_64 or aarch64\n>    - macOS arm64 (Apple Silicon)\n>    - Windows 10 or 11 x86_64, building inside WSL2. Nix does not run on Windows itself, so Windows [packages](https://docs.logos.co/get-started/glossary.md#package) are cross-compiled from Linux. See [Build a Windows package](#build-a-windows-package).\n> - At least 10 GB of disk space\n> - Git\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n> - Basic familiarity with C++ (C++17), CMake, and Nix concepts\n\n## What to expect\n\n- You can scaffold and build a Logos core module from the `logos-module-builder` template.\n- You can inspect the compiled module's metadata, methods and events with the `lm` CLI tool.\n- You can package the module, install it with `logosctl` and call its methods, or call them from Logos Basecamp.\n\n## Step 1: Scaffold the module project\n\nThe `logos-module-builder` provides a template for each kind of module. The default template produces a minimal core module: one C++ class and no UI.\n\n1. Create a new directory using your module's name and initialise it from the module builder template. Replace `<module-name>` with your module's name, for example, `my_module`.\n\n   ```bash\n   mkdir <module-name> && cd <module-name>\n   nix flake init -t github:logos-co/logos-module-builder/0.3.2\n   ```\n\n   - The other templates are `#with-external-lib` (see [Wrap a C library as a Logos module](https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md)), `#ui-qml-backend` (see [Build a Logos C++ UI module](https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module.md)), `#ui-qml`, `#rust` and `#rust-with-external-lib`.\n\n1. Review the project directory. The generated project structure looks like this:\n\n   ```text\n   <module-name>/\n   ├── flake.nix\n   ├── metadata.json\n   ├── CMakeLists.txt\n   └── src/\n       ├── minimal_impl.h\n       └── minimal_impl.cpp\n   ```\n\n   The template is a complete module named `minimal`. `src/minimal_impl.h` declares its API, two methods (`greet` and `getStatus`) and an event (`greeted`), and the build generates the Qt plugin from it. You replace the `minimal` placeholders with your module's name in Step 2.\n\n   > **Info**\n   >\n   > The `metadata.json` file is the single source of truth for your module. Read [LGX package format and bundling reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md) for more details.\n\n## Step 2: Adapt the template for your module\n\nRename the `minimal` placeholders in the generated files to your module's name, then replace the example methods with your own. In this step, `<ModuleName>` is your module's name in PascalCase, for example, `MyModule` for `my_module`.\n\n1. Edit `metadata.json` and set `name`, `display_name`, `description`, and `main` to match your module.\n   - `name` must be a valid C identifier; it is used in file names, method calls, and module loading.\n   - `main` is `<module-name>_plugin`, the plugin file name without its extension (`.so` or `.dylib`).\n   - Leave the other fields in place, including `\"interface\": \"universal\"` and the `nix` block, which the build and the bundler rely on.\n\n1. Rename the source files in `src/` to match your module name.\n\n   ```bash\n   mv src/minimal_impl.h   src/<module-name>_impl.h\n   mv src/minimal_impl.cpp src/<module-name>_impl.cpp\n   ```\n\n   - The build looks for `src/<module-name>_impl.h` and a class named `<ModuleName>Impl`. To use other names, set `codegen.impl_header` and `codegen.impl_class` in `metadata.json`.\n\n1. Edit both source files: rename the class `MinimalImpl` to `<ModuleName>Impl`, and make `src/<module-name>_impl.cpp` include `<module-name>_impl.h`.\n\n1. Edit `CMakeLists.txt` and update the `project()` name and the two `SOURCES` entries to the renamed files.\n   - `logos_module()` reads `NAME` from `metadata.json`, so the two cannot drift apart.\n\n1. Edit `flake.nix` and update the `description` field.\n   - The generated `flake.nix` uses an unpinned `logos-module-builder` URL. For reproducible builds, pin it: `logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.2\";`.\n\n1. Replace the example methods and event in `src/<module-name>_impl.h` and `src/<module-name>_impl.cpp` with your own, or keep them for now to follow the examples in this procedure.\n   - Every `public` method is part of the module's API. Use the parameter and return types in the table in [Wrap a C library as a Logos module](https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md#step-3-configure-the-logos-module), for example `int64_t` and `std::string`, not `int` or `QString`.\n   - A `///` comment directly above a method or event becomes its description, which `lm`, `logosctl module show` and Logos Basecamp display.\n\n> **Tip**\n>\n> Run `grep -ri \"minimal\" .` after editing to catch any remaining placeholder references (`minimal`, `Minimal`, `MinimalImpl`) before building.\n\n## Step 3: Build the module\n\n1. Initialise a Git repository.\n\n   ```bash\n   git init && git add -A\n   ```\n\n   - Nix only sees files that Git tracks. Run `git add` again after you add new files.\n\n1. Build the full module output (library and generated SDK headers).\n\n   ```bash\n   nix build\n   ```\n\n   - Use `nix build '.#lib'` to build only the plugin shared library.\n   - Use `nix build '.#include'` to build only the generated SDK headers.\n   - The first build downloads Qt, the Logos SDK, and the code generators, and can take several minutes.\n\n1. Verify the build output contains the plugin binary and generated headers:\n\n   ```text\n   result/\n   ├── lib/\n   │   └── <module-name>_plugin.so   # (or .dylib on macOS)\n   └── include/\n       ├── <module-name>_api.h       # Generated type-safe wrapper header\n       └── <module-name>_api.cpp     # Generated wrapper implementation\n   ```\n\n## Step 4: Inspect your module\n\nThe [`lm`](https://docs.logos.co/get-started/glossary.md#lm) tool (from `logos-module`) inspects compiled module binaries without loading them into a runtime.\n\n1. Build the `lm` tool from the `logos-module` repository.\n\n   ```bash\n   nix build 'github:logos-co/logos-module/0.3.0#lm' --out-link ./lm\n   ```\n\n1. View the module metadata and confirm the information is correct.\n\n   ```bash\n   # Linux\n   ./lm/bin/lm metadata result/lib/<module-name>_plugin.so\n\n   # macOS\n   ./lm/bin/lm metadata result/lib/<module-name>_plugin.dylib\n   ```\n\n   - Append `--json` for JSON output.\n\n   For example, for `my_module` with the display name `My Module`:\n\n   ```text\n   Plugin Metadata:\n   ================\n   Name:         my_module\n   Display name: My Module\n   Version:      1.0.0\n   Description:  My first Logos module\n   Author:\n   Type:         core\n   Protocol:     0.9.0\n   Dependencies: (none)\n   ```\n\n1. View the module methods.\n\n   ```bash\n   # Linux\n   ./lm/bin/lm methods result/lib/<module-name>_plugin.so\n\n   # macOS\n   ./lm/bin/lm methods result/lib/<module-name>_plugin.dylib\n   ```\n\n   - Append `--json` for JSON output.\n   - `lm events` lists the module's events the same way.\n\n   With the template's example methods, the output looks like this:\n\n   ```text\n   Plugin Methods:\n   ===============\n\n   tstr greet(tstr name)\n     Signature: greet(tstr)\n     Invokable: yes\n     Description: Returns a greeting and announces it as a typed `greeted` event.\n\n   tstr getStatus()\n     Signature: getStatus()\n     Invokable: yes\n     Description: Returns a short status string.\n\n   tstr name()\n     Signature: name()\n     Invokable: yes\n     Description: The module's name, as declared in its metadata.\n\n   tstr version()\n     Signature: version()\n     Invokable: yes\n     Description: The module's version, as declared in its metadata.\n\n   tstr lidl()\n     Signature: lidl()\n     Invokable: yes\n     Description: The module's canonical LIDL interface document.\n   ```\n\n   - Types are shown as the module's interface contract publishes them: `tstr` is a string, and `int` is a 64-bit integer.\n   - `name()`, `version()` and `lidl()` are generated for every module. `lidl()` returns the module's interface contract.\n\n1. To browse the methods in a window instead, build the graphical `logos-module-viewer` and open the module with it:\n\n   ```bash\n   nix build 'github:logos-co/logos-module-viewer/f8914691f3ac8be78b894d01b5d5ec6ce25d44f3#app' --out-link ./logos-viewer\n\n   # Linux\n   ./logos-viewer/bin/logos-module-viewer -m ./result/lib/<module-name>_plugin.so\n\n   # macOS\n   ./logos-viewer/bin/logos-module-viewer -m ./result/lib/<module-name>_plugin.dylib\n   ```\n\n   - The viewer has no release yet, so the command pins the commit this procedure was tested with.\n   - The viewer only loads modules that don't call other modules.\n\n## Step 5: Package the module\n\nBefore you can run your module with `logosctl` or install it into Logos Basecamp, you need to package the build output into an [LGX](https://docs.logos.co/get-started/glossary.md#lgx) package. Check out the [LGX package format and bundling reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md) for more details on the format and bundling options.\n\n> **Info**\n>\n> The `manifest.json` is auto-generated from your module's `metadata.json` by the bundler. It maps each variant to its main entry point.\n\n1. Build the portable package, which bundles every dependency and has no `/nix/store` references:\n\n   ```bash\n   nix build '.#lgx-portable' --out-link result-lgx-portable\n   ```\n\n   - Released builds of `logosctl` and Logos Basecamp install only portable variants (for example, `linux-amd64` or `darwin-arm64`).\n\n1. Build the development package, for development builds of Logos Basecamp:\n\n   ```bash\n   nix build '.#lgx' --out-link result-lgx\n   ```\n\n   - The development variant (for example, `darwin-arm64-dev`) references `/nix/store` paths, so it works only on a machine with that Nix store. Development builds of `logosctl` and Logos Basecamp install only `-dev` variants.\n\n1. Check that each output holds the package, `logos-<module-name>-module-lib.lgx`:\n\n   ```bash\n   ls result-lgx-portable/ result-lgx/\n   ```\n\n### Build a Windows package\n\nWindows packages are cross-compiled: Nix runs on Linux and targets `x86_64-windows`. On a Windows machine, that Linux is WSL2. The result installs with the Windows build of `logosctl`.\n\n1. Set up WSL2 with Ubuntu. In PowerShell, run:\n\n   ```powershell\n   wsl --install -d Ubuntu-24.04\n   ```\n\n   - Restart Windows if prompted, then open **Ubuntu 24.04** from the Start menu and create your Linux user.\n   - WSL2 needs hardware virtualisation. If Windows itself runs in a virtual machine, enable nested virtualisation for that VM.\n\n1. In the Ubuntu terminal, install Nix and open a new terminal afterwards:\n\n   ```bash\n   sh <(curl -L https://nixos.org/nix/install) --daemon\n   ```\n\n1. Enable flakes and the Logos binary cache, which serves the prebuilt Windows toolchain (Qt, the MinGW libraries, and the Logos SDK) so that you don't compile it yourself. The Nix daemon reads these settings from `/etc/nix/nix.conf`:\n\n   ```bash\n   sudo tee -a /etc/nix/nix.conf <<'EOF'\n   experimental-features = nix-command flakes\n   extra-substituters = https://cache.nix.logos.co/public\n   extra-trusted-public-keys = public:l4HrXgL4nw246+LBh2SOJyhz64BoGegOYLheT/iIAPU=\n   EOF\n   sudo systemctl restart nix-daemon\n   ```\n\n1. Scaffold or clone your module in the Linux file system, for example under `~/`, rather than under `/mnt/c`. Nix and Git are much slower on the Windows drive.\n\n1. Build the portable Windows package:\n\n   ```bash\n   nix build .#packages.x86_64-windows.lgx-portable\n   ```\n\n   - `result/logos-<module-name>-module-lib.lgx` holds a `windows-x86_64` variant whose plugin is a `.dll`.\n   - Build the portable variant: a `-dev` variant references `/nix/store` paths that don't exist on Windows.\n   - Every dependency of your module needs a Windows build too.\n\n1. Copy the package to your Windows user folder:\n\n   ```bash\n   cp -L result/logos-<module-name>-module-lib.lgx /mnt/c/Users/<windows-user>/Downloads/\n   ```\n\n1. In PowerShell, with `logosctl` installed by `install-logosctl.ps1`, install, load, and call the module. Confirm the install prompt, or pass `-y`:\n\n   ```powershell\n   logosctl daemon start --detach\n   logosctl package install --file $HOME\\Downloads\\logos-<module-name>-module-lib.lgx\n   logosctl module load <module-name>\n   logosctl call <module-name> <method> <args>\n   ```\n\n   The first build in a fresh WSL2 Ubuntu downloads about 500 MB from the binary cache and takes a few minutes; later builds reuse it.\n\n## Step 6: Install the module\n\nBefore you can run your module with `logosctl`, install the LGX package. `logosctl package install` unpacks it into the current [session's](https://github.com/logos-co/logos-logoscore-cli/blob/master/docs/logosctl.md#sessions) `modules/` directory (`~/.logosctl/modules/` unless `--config-dir` or `LOGOSCTL_CONFIG_DIR` says otherwise), the same session that `logosctl daemon start` reads from.\n\nThere are two ways to install a module:\n\n- Install a locally built `.lgx` package\n- Install a package by name from the catalogue\n\n### Install a locally built `.lgx` package\n\n1. Start the `logosctl` daemon in detached mode.\n\n   ```bash\n   logosctl daemon start --detach\n   ```\n\n   - The command returns once the daemon accepts commands.\n\n1. Install the portable `.lgx` package:\n\n   ```bash\n   logosctl package install --file ./result-lgx-portable/*.lgx -y\n   ```\n\n   - `-y` applies the install without asking for confirmation.\n   - Use `--dir` instead of `--file` to install all LGX packages in a directory at once: `logosctl package install --dir ./packages/ -y`\n\n1. Verify the installed module:\n\n   ```bash\n   logosctl package ls\n   ```\n\n   Your module is listed with the source `user`, next to the packages embedded in `logosctl`. The session's `modules/` directory now contains a subdirectory with `manifest.json`, the plugin binary (`.so` or `.dylib`), and a `variant` file.\n\n### Install a package by name from the catalogue\n\nThe Logos module [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue) is hosted on GitHub Releases in the [logos-modules-release](https://github.com/logos-co/logos-modules-release) repository. `logosctl package install` fetches a named package straight from the catalogue and installs it in one step.\n\n> **Warning**\n>\n> Catalogue packages ship portable variants only (for example, `linux-amd64`, `darwin-arm64`). They install into `logosctl` and into released builds of Logos Basecamp, but not into a development build of Logos Basecamp, which expects `-dev` variants.\n\n1. Start the `logosctl` daemon in detached mode, if it is not running yet.\n\n   ```bash\n   logosctl daemon start --detach\n   ```\n\n1. Refresh the catalogue, then search it for the module you want to install. Replace `<query>` with what you're looking for (for example, `chat`).\n\n   ```bash\n   logosctl catalog refresh\n   logosctl package search <query>\n   ```\n\n   > **Tip**\n   >\n   > Run `logosctl package search` with no query to browse all available packages.\n\n1. Install it by the name from the search results (for example, `chat_module`). `logosctl` resolves the name against the catalogue, downloads the package and its dependencies, and unpacks them into the session's `modules/` (or `plugins/`, for a UI module) in one step.\n\n   ```bash\n   logosctl package install <module-name> --yes\n   ```\n\n   - Add `--version` to pin a specific release instead of the newest one, and `--root-hash` to pin the exact published package identity.\n\n## Step 7: Run the module\n\nThere are two Logos runtimes that can load and run your module: `logosctl` and Logos Basecamp.\n\n### Run with `logosctl`\n\nThe `logosctl` CLI (from [`logos-logoscore-cli`](https://github.com/logos-co/logos-logoscore-cli)) is a headless runtime that can load modules and invoke their methods from the command line. It runs as a daemon that stays alive to host modules.\n\n1. Load the module and call a method. Replace `<method>` and `<args>` with the method name and arguments you want to call.\n\n   ```bash\n   logosctl module load <module-name>\n   logosctl call <module-name> <method> <args>\n   ```\n\n   - With the template's example methods, `logosctl call my_module greet World` prints `Hello, World! Greetings from the minimal module.`\n   - `logosctl module show <module-name>` lists the module's methods and events with their descriptions.\n\n1. Stop the daemon when finished.\n\n   ```bash\n   logosctl daemon stop\n   ```\n\n> **Tip**\n>\n> Check out [Logos CLI Reference](https://docs.logos.co/core/reference/logos-cli-reference.md) for more details on available commands and options.\n\n### Run with Logos Basecamp\n\nLogos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp) is a local-first launcher for peer-to-peer apps built on Logos, with a graphical interface for managing and running modules. Its **Module Inspector** lists every loaded core module and lets you call its methods, so you can try your module without writing a UI module for it.\n\nThese steps use a development build of Logos Basecamp, which installs the development package from Step 5, with a data directory of its own. To use the released Logos Basecamp instead, install the portable package as described in [Install and load a module in Basecamp](https://docs.logos.co/basecamp/install-and-load-module-in-basecamp.md).\n\n1. Build the development version of Logos Basecamp and the `lgpm` package manager, which installs packages into a directory of your choice:\n\n   ```bash\n   nix build 'github:logos-co/logos-basecamp/0.3.2' --out-link ./logos-basecamp\n   nix build 'github:logos-co/logos-package-manager/0.3.0#cli' --out-link ./pm\n   ```\n\n1. Create a data directory for Logos Basecamp, and install the development package into its `modules/` directory:\n\n   ```bash\n   mkdir -p basecamp-data/modules basecamp-data/plugins\n   ./pm/bin/lgpm --modules-dir basecamp-data/modules install --file ./result-lgx/*.lgx\n   ```\n\n   - `lgpm` warns that the package is unsigned, which is expected for a package you built yourself.\n\n1. Launch Logos Basecamp with that data directory:\n\n   ```bash\n   ./logos-basecamp/bin/LogosBasecamp --user-dir \"$PWD/basecamp-data\"\n   ```\n\n   - `--user-dir` keeps this data apart from any other Logos Basecamp you run. Logos Basecamp reads core modules from its `modules/` subdirectory and UI modules from `plugins/`.\n\n1. Open **Settings**, select **Module Inspector**, and click **Load** in your module's row.\n\n   **Expected result:** the button changes to **Unload** once the module is loaded.\n\n1. Click **Interface** in your module's row.\n\n   **Expected result:** each method is listed with its signature, its description and a **Call** button.\n\n1. Click **Call** next to a method that takes no arguments, such as the template's `getStatus`.\n\n   **Expected result:** the **Result** area shows what the method returned, as JSON. For `getStatus`, that is `{\"result\":\"Minimal module is running.\"}`.\n\n   - **Call** passes no arguments, so a method that takes parameters reports `invalid_args`. Call those methods with `logosctl call` instead.\n\n> **Tip**\n>\n> Try running the [Blockchain module](https://docs.logos.co/blockchain/get-started/run-blockchain-node-from-cli.md), [Storage module](https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md) or [Chat module](https://docs.logos.co/messaging/get-started/send-1-1-messages-logos-chat.md) or browse the [Logos module catalogue](https://github.com/logos-co/logos-modules-release#module-set). `logosctl package search` lists it from the command line.\n\n## Troubleshooting\n\n### Known constraints\n\nA single `logosctl` daemon instance supports only one instance of a module, so all dependent apps share that state. To run multiple independent instances from the CLI, start separate daemons under separate sessions (`logosctl daemon start --config-dir <dir>` for each), so each gets its own installed modules and state. From Logos Basecamp, start separate instances with their own user directories (`--user-dir`); each one runs its own embedded runtime and module instances in isolation.\n\n### Nix reports an \"experimental features\" error\n\nIf you see errors about experimental features, either pass the flag:\n\n```bash\nnix --extra-experimental-features \"nix-command flakes\" build\n```\n\nOr add the following to `~/.config/nix/nix.conf`:\n\n```conf\nexperimental-features = nix-command flakes\n```\n\n### The build fails with `Failed to open header file` or `unknown type name`\n\nThe build derives the implementation from the module's `name` in `metadata.json`: it parses `src/<module-name>_impl.h` and expects a class named `<ModuleName>Impl` in it.\n\n- A header with another name stops the build with `Error parsing impl header: Failed to open header file: src/<module-name>_impl.h`.\n- A class with another name fails in the generated code with `error: unknown type name '<ModuleName>Impl'`.\n\nRename the file or the class to match `name`, or set `codegen.impl_header` and `codegen.impl_class` in `metadata.json` to the names you use.\n\n### The build reports that the module declares no `interface`\n\nA core module must keep `\"interface\": \"universal\"` in `metadata.json`. Without it, the build stops with `logos-module-builder: module '<module-name>' is a core module shipping a plugin (main: <module-name>_plugin) but declares no interface`. Add the field back.\n\n### Module not discovered by Logos Basecamp\n\nConfirm that you launched Logos Basecamp with the `--user-dir` you installed into, and that the module is in a subdirectory of its `modules/` directory (for example, `basecamp-data/modules/my_module/`) containing the module binary and `manifest.json`. A development build of Logos Basecamp needs the `-dev` variant from `.#lgx`; a released build needs the portable variant from `.#lgx-portable`.\n\nAlso confirm you installed the module into the base directory the running instance actually reads, and restarted it afterwards. A rebuilt package never reaches a running Basecamp on its own. See [Troubleshoot Logos module development with Basecamp](https://docs.logos.co/build-an-app/scaffold/troubleshooting/troubleshoot-logos-module-development-with-basecamp.md).\n\n### Module not discovered by `logosctl`\n\nConfirm the module is in a subdirectory of the session's `modules/` directory (for example, `modules/my_module/`) and that the subdirectory contains a `manifest.json` with a `main` object matching your OS and architecture. Run `logosctl package ls` to see what the current session actually has installed.\n\n### `logosctl package install` fails\n\nVerify the session's `modules/` directory (under `--config-dir`, default `~/.logosctl`) exists and is writable. If installing from a local `.lgx` file, confirm the file path is correct (the bundler writes `logos-<module-name>-module-lib.lgx`, not `<module-name>.lgx`). If installing from the catalogue, check your internet connection and that you've run `logosctl catalog refresh` recently. To pin a specific version instead of the newest one, pass `--version` (and optionally `--root-hash`) to `logosctl package install`.\n\nIf installing into Logos Basecamp's data directory with the standalone `lgpm`, verify that the target directory exists and is writable instead.\n\n### LGX variant mismatch\n\nIf `logosctl package install` fails with an error such as `Package does not contain variant for platform: darwin-arm64 (package provides: darwin-arm64-dev)`, the package has no variant of the kind that `logosctl` installs.\n\n- `nix build '.#lgx'` produces a single `-dev` variant (for example, `linux-amd64-dev`) for development builds of `logosctl` and Logos Basecamp. Released builds reject it.\n- `nix build '.#lgx-portable'` produces a single portable variant (for example, `linux-amd64`) for released builds of `logosctl` and Logos Basecamp.\n\nCatalogue packages ship portable variants only. To put both variants, or variants for several platforms, in one package, see the [LGX package format and bundling reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md).\n\n### `nix build .#lib` does nothing or fails silently\n\nSome shells (notably zsh) treat `#` as a comment character outside quotes. Quote the flake reference so the `#` reaches Nix intact.\n\n```bash\nnix build '.#lib'\n```\n\n### First build is slow\n\nThe first `nix build` downloads Qt 6, the Logos C++ SDK, the code generator, and the rest of the build dependencies. This is a one-time cost; subsequent builds reuse the cache.\n"},{"id":"core/build-modules/wrap-a-c-library-as-a-logos-core-module","title":"Wrap a C library as a Logos module","summary":"Expose functions from a C shared library through a Logos core module.","section":"core","doc_type":"procedure","url":"https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module","markdown_url":"https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md","permalink":"/core/build-modules/wrap-a-c-library-as-a-logos-core-module","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Scaffold the module project"},{"depth":2,"text":"Step 2: Write the C library"},{"depth":2,"text":"Step 3: Configure the Logos module"},{"depth":2,"text":"Step 4: Build the module"},{"depth":2,"text":"Step 5: Inspect the module"},{"depth":2,"text":"Step 6: Install the module with `logosctl`"},{"depth":2,"text":"Step 7: Call the module from `logosctl`"},{"depth":2,"text":"Step 8: Unit-test the module"},{"depth":2,"text":"Troubleshooting Logos module wrapping"},{"depth":3,"text":"A method doesn't appear in `lm` or can't be called"},{"depth":3,"text":"The build fails with an unknown type or a method the generator can't parse"},{"depth":3,"text":"The plugin fails with `Cannot load library`"},{"depth":3,"text":"Events never reach subscribers"},{"depth":3,"text":"`logosctl` cannot find the module"},{"depth":3,"text":"`nix build .#lib` does nothing or fails silently"},{"depth":3,"text":"The first build is slow"},{"depth":3,"text":"The build fails with undefined symbols from the C library"}],"markdown":"# Wrap a C library as a Logos module\n\n#### Expose functions from a C shared library through a Logos core module.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure walks you through wrapping a C shared library (`.so` on Linux, `.dylib` on macOS) as a Logos [module](https://docs.logos.co/get-started/glossary.md#module). You write one plain C++ class, with no Qt code and no plugin boilerplate, and `logos-module-builder` generates the Qt plugin around it. By the end, you have a `calc_module` that builds, loads into `logosctl` and answers method calls, and unit tests that run it against a mock of the C library.\n\nFor a production example, see [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module), which wraps the `nim-libp2p` library compiled to a C shared library.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux x86_64 or aarch64\n>    - macOS arm64 (Apple Silicon)\n> - Git\n> - A C compiler (`gcc` or `clang`) to build the C library.\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n> - Basic familiarity with C and C++.\n\n## What to expect\n\n- You can wrap a C library in a Logos module by writing one plain C++ class, and let the build generate the Qt plugin around it.\n- You can inspect the built module with `lm` and call its methods from the command line with `logosctl`.\n- You can unit-test the module against a link-time mock of the C library, without starting a daemon.\n\n## Step 1: Scaffold the module project\n\nThe `with-external-lib` template of `logos-module-builder` provides the `flake.nix`, `metadata.json`, `CMakeLists.txt` and directory layout that a module wrapping a library needs.\n\n1. Create the project directory and initialise it from the template:\n\n   ```bash\n   mkdir logos-calc-module && cd logos-calc-module\n   nix flake init -t github:logos-co/logos-module-builder/0.3.2#with-external-lib\n   ```\n\n   - The template already follows the pure C++ pattern that this procedure uses: one plain class in `src/external_lib_impl.h` and `src/external_lib_impl.cpp`, and `\"interface\": \"universal\"` in `metadata.json`.\n   - For a module that wraps no external library, use the default template instead: `nix flake init -t github:logos-co/logos-module-builder/0.3.2`.\n\n1. Remove the template's example class. The following steps replace it with the sources of `calc_module`:\n\n   ```bash\n   rm -f src/external_lib_impl.h src/external_lib_impl.cpp\n   ```\n\n## Step 2: Write the C library\n\nCreate the C library that the module wraps, in the `lib/` directory. To wrap an existing library instead, place its pre-built `.so` or `.dylib` and its header file in `lib/` and continue at [Step 3](#step-3-configure-the-logos-module).\n\n1. Create the `lib` directory:\n\n   ```bash\n   mkdir -p lib\n   ```\n\n1. Create `lib/libcalc.h`:\n\n   ```c\n   #ifndef LIBCALC_H\n   #define LIBCALC_H\n\n   #ifdef __cplusplus\n   extern \"C\" {\n   #endif\n\n   /** Add two integers. */\n   int calc_add(int a, int b);\n\n   /** Multiply two integers. */\n   int calc_multiply(int a, int b);\n\n   /** Compute factorial of n (n must be >= 0). Returns -1 on error. */\n   int calc_factorial(int n);\n\n   /** Compute the nth Fibonacci number (n must be >= 0). Returns -1 on error. */\n   int calc_fibonacci(int n);\n\n   /** Return the library version string. Caller must NOT free. */\n   const char* calc_version(void);\n\n   #ifdef __cplusplus\n   }\n   #endif\n\n   #endif /* LIBCALC_H */\n   ```\n\n   The `extern \"C\"` block prevents C++ name mangling, so the module can find the symbols.\n\n1. Create `lib/libcalc.c`:\n\n   ```c\n   #include \"libcalc.h\"\n\n   int calc_add(int a, int b)\n   {\n       return a + b;\n   }\n\n   int calc_multiply(int a, int b)\n   {\n       return a * b;\n   }\n\n   int calc_factorial(int n)\n   {\n       if (n < 0) return -1;\n       if (n <= 1) return 1;\n       int result = 1;\n       for (int i = 2; i <= n; i++) {\n           result *= i;\n       }\n       return result;\n   }\n\n   int calc_fibonacci(int n)\n   {\n       if (n < 0) return -1;\n       if (n == 0) return 0;\n       if (n == 1) return 1;\n       int a = 0, b = 1;\n       for (int i = 2; i <= n; i++) {\n           int tmp = a + b;\n           a = b;\n           b = tmp;\n       }\n       return b;\n   }\n\n   const char* calc_version(void)\n   {\n       return \"1.0.0\";\n   }\n   ```\n\n1. Build the shared library:\n\n   ```bash\n   cd lib\n\n   # Linux\n   gcc -shared -fPIC -o libcalc.so libcalc.c\n\n   # macOS\n   # gcc -shared -fPIC -o libcalc.dylib libcalc.c\n\n   cd ..\n   ```\n\n1. Confirm that the symbols are exported:\n\n   ```bash\n   # Linux\n   nm -D lib/libcalc.so | grep calc\n\n   # macOS\n   # nm -gU lib/libcalc.dylib | grep calc\n   ```\n\n   Each function is listed with `T` (the text section). Addresses vary, and macOS prefixes each name with `_`:\n\n   ```text\n   0000000000001139 T calc_add\n   0000000000001179 T calc_factorial\n   00000000000011f5 T calc_fibonacci\n   0000000000001159 T calc_multiply\n   0000000000001299 T calc_version\n   ```\n\n## Step 3: Configure the Logos module\n\nWith the pure C++ (`universal`) pattern you write a single C++ class. `metadata.json`, `CMakeLists.txt` and `flake.nix` tell the build system the rest, and `logos-cpp-generator` generates the Qt plugin around the class. After this step, the project looks like this:\n\n```text\nlogos-calc-module/\n├── flake.nix          # Nix build configuration (~10 lines)\n├── metadata.json      # Module metadata, build settings, and runtime config\n├── CMakeLists.txt     # CMake build file\n├── lib/\n│   ├── libcalc.h      # C library header\n│   └── libcalc.c      # C library source (compiled by CMake)\n└── src/\n    ├── calc_module_impl.h     # Plain C++ class (no Qt, no plugin macros)\n    └── calc_module_impl.cpp   # Implementation (wrapping logic)\n```\n\n1. Replace `metadata.json` with the following. Set `\"interface\": \"universal\"` and declare the library under `nix.external_libraries`:\n\n   ```json\n   {\n     \"name\": \"calc_module\",\n     \"version\": \"1.0.0\",\n     \"type\": \"core\",\n     \"category\": \"general\",\n     \"description\": \"Calculator module wrapping libcalc C library\",\n     \"main\": \"calc_module_plugin\",\n     \"interface\": \"universal\",\n     \"dependencies\": [],\n\n     \"nix\": {\n       \"packages\": {\n         \"build\": [],\n         \"runtime\": []\n       },\n       \"external_libraries\": [\n         {\n           \"name\": \"calc\",\n           \"vendor_path\": \"lib\"\n         }\n       ],\n       \"cmake\": {\n         \"find_packages\": [],\n         \"extra_sources\": [],\n         \"extra_include_dirs\": [\"lib\"],\n         \"extra_link_libraries\": []\n       }\n     }\n   }\n   ```\n\n   This file is the single source of truth for the module. It is embedded in the plugin, read by `logos-module-builder` to configure the Nix build, used by CMake to link the external library, and used to write the [LGX](https://docs.logos.co/get-started/glossary.md#lgx) package manifest.\n\n   | Field | What it does |\n   | --- | --- |\n   | `name` | Module name. It must be a valid C identifier, because it is used in file names and method calls. |\n   | `main` | The name of the generated plugin, `<name>_plugin`. You don't write this file: the build produces `calc_module_plugin.so` or `calc_module_plugin.dylib`. |\n   | `interface` | `\"universal\"` selects the pure C++ pattern. The build parses `src/calc_module_impl.h` into the module's interface contract and generates the Qt plugin from it. |\n   | `nix.external_libraries` | The C or C++ libraries the module wraps. `name` is the CMake target and `vendor_path` is the directory that holds the source or binary. The build links each library into the plugin. |\n   | `nix.cmake.extra_include_dirs` | Directories added to the include path, so your C++ code can `#include \"lib/libcalc.h\"`. |\n\n1. Replace `CMakeLists.txt` with the following:\n\n   ```cmake\n   cmake_minimum_required(VERSION 3.14)\n   project(CalcModulePlugin LANGUAGES CXX)\n\n   # Include the Logos Module CMake helper (provided by logos-module-builder)\n   if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})\n       include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)\n   elseif(EXISTS \"${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake\")\n       include(cmake/LogosModule.cmake)\n   else()\n       message(FATAL_ERROR \"LogosModule.cmake not found\")\n   endif()\n\n   # Define the module with its external library dependency.\n   # Because metadata.json sets `interface: universal`, the builder runs\n   # logos-cpp-generator over src/calc_module_impl.h before configuring,\n   # and LogosModule.cmake compiles the generated glue automatically.\n   logos_module(\n       NAME calc_module\n       SOURCES\n           src/calc_module_impl.h\n           src/calc_module_impl.cpp\n       EXTERNAL_LIBS\n           calc\n   )\n   ```\n\n   - List only your own source files under `SOURCES`. `LogosModule.cmake` compiles the generated plugin code automatically.\n   - `NAME` must match `name` in `metadata.json`, and `EXTERNAL_LIBS` must match `nix.external_libraries[].name`. If `NAME` differs, the build can succeed and the install phase still fail, because it looks for the plugin file named after `metadata.json`.\n   - `logos_module()` finds `libcalc.so` or `libcalc.dylib` in `lib/`, links it to the plugin and sets the RPATH so the plugin finds it at runtime.\n   - The `if`/`elseif`/`else` block is boilerplate. Don't change it.\n\n1. Replace `flake.nix` with the following. It pins `logos-module-builder` to the release this procedure was tested with:\n\n   ```nix\n   {\n     description = \"Calculator module - wraps libcalc C library for Logos\";\n\n     inputs = {\n       logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.2\";\n     };\n\n     outputs = inputs@{ logos-module-builder, ... }:\n       logos-module-builder.lib.mkLogosModule {\n         src = ./.;\n         configFile = ./metadata.json;\n         flakeInputs = inputs;\n       };\n   }\n   ```\n\n   - `mkLogosModule` fetches Qt, the Logos SDK and the code generators, and derives the build from `metadata.json`.\n   - To depend on another module, add it as a flake input named after the `name` in that module's `metadata.json`. For example, a dependency on `waku_module` is the input `waku_module.url = \"github:logos-co/logos-waku-module\"`.\n   - To fetch and build the library from source instead of placing it in `lib/`, see [Wrapping a library from a flake input](https://github.com/logos-co/logos-tutorial/blob/master/outputs/tutorial-wrapping-c-library.md#advanced-wrapping-a-library-from-a-flake-input) in the Logos tutorials.\n\n1. Create `src/calc_module_impl.h`. This is the only interface you write, and it is plain C++:\n\n   ```cpp\n   #pragma once\n\n   #include <cstdint>\n   #include <string>\n\n   #include <logos_module_context.h>  // LogosModuleContext base + `logos_events:`\n\n   // Include the C library header (extern \"C\" already in the header).\n   extern \"C\" {\n       #include \"lib/libcalc.h\"\n   }\n\n   class CalcModuleImpl : public LogosModuleContext {\n   public:\n       CalcModuleImpl() = default;\n       ~CalcModuleImpl() = default;\n\n       // ── Public API — every method here is callable over IPC ──────────\n       // The generator maps C++ types onto the contract automatically:\n       //   int64_t  ↔ int      std::string ↔ tstr      bool ↔ bool\n       //\n       // A doc comment directly above a method becomes that method's\n       // `description` in the module's method introspection — surfaced\n       // by `lm`, `logosctl module show`, and Basecamp's Methods list.\n       // Use `///` (one or more lines) or a `/** ... */` block; the\n       // comment's line breaks are preserved. (Plain `//` comments like\n       // this block are ignored, so they never leak into the API.)\n\n       /// Adds two integers and returns the sum.\n       int64_t add(int64_t a, int64_t b);\n\n       /// Multiplies two integers and returns the product.\n       int64_t multiply(int64_t a, int64_t b);\n\n       // A multi-line description: consecutive `///` lines keep their breaks.\n       /// Computes the factorial n! of a non-negative integer.\n       /// Defined as n * (n-1) * ... * 1, with 0! = 1.\n       int64_t factorial(int64_t n);\n\n       /// Returns the nth Fibonacci number (0-indexed).\n       int64_t fibonacci(int64_t n);\n\n       // A `/** ... */` block comment works too (line breaks preserved).\n       /**\n        * Returns the version string of the wrapped libcalc C library.\n        * Read straight from the linked native library, not metadata.json.\n        */\n       std::string libVersion();\n\n       /// Looks up the library version and emits it as a `versionReady`\n       /// event instead of returning it. Used by the QML tutorial (Part 2).\n       void libVersionNotify();\n\n       // ── Events ───────────────────────────────────────────────────────\n       // Declared like Qt signals. The generator emits the body (in\n       // calc_module_events.cpp) that routes the typed args to subscribers\n       // via the host's `eventResponse` mechanism. QML subscribes with\n       // logos.onModuleEvent(\"calc_module\", \"versionReady\").\n       //\n       // A `///` doc comment documents the event too — it surfaces as the\n       // event's `description` alongside methods (`lm events`, `logosctl\n       // module show`, and Basecamp's Interface screen).\n   logos_events:\n       /// Emitted by libVersionNotify() once the library version is known.\n       /// Carries the version string read from libcalc.\n       void versionReady(const std::string& version);\n   };\n   ```\n\n   - Every `public` method is callable by other modules and by `logosctl`. `private` members are not exposed.\n   - A doc comment directly above a method or event (`///`, or a `/** ... */` block) becomes its description, which `lm`, `logosctl module show` and Basecamp display. Plain `//` comments are ignored.\n   - Events are declared in a `logos_events:` section. Inheriting `LogosModuleContext` lets the class emit them and call other modules.\n   - Use `int64_t` for integers, not `int`. The code generator reads the header as text and recognises only the types in this table:\n\n   | C++ type | Contract type | A Qt caller sees |\n   | --- | --- | --- |\n   | `void` | `void` | `void` |\n   | `bool` | `bool` | `bool` |\n   | `int64_t` | `int` | `qlonglong` |\n   | `uint64_t` | `uint` | `qulonglong` |\n   | `double` | `float64` | `double` |\n   | `std::string` | `tstr` | `QString` |\n   | `std::vector<std::string>` | `[tstr]` | `QStringList` |\n   | `std::vector<uint8_t>` | `bstr` | `QByteArray` |\n   | `LogosMap` / `LogosList` (from `<logos_json.h>`) | `{tstr: any}` / `[any]` | `QVariantMap` / `QVariantList` |\n   | `StdLogosResult` | `result` | `LogosResult` (from `<logos_result.h>`) |\n\n   The contract type is what the module publishes about itself, in LIDL, its interface definition language. It is what `lm` prints in [Step 5](#step-5-inspect-the-module), and what a caller in any language binds to. The last column is what a C++ caller built on Qt compiles against.\n\n1. Create `src/calc_module_impl.cpp`. Each method calls the C function, converts the result to a C++ type if needed, and returns it:\n\n   ```cpp\n   #include \"calc_module_impl.h\"\n\n   int64_t CalcModuleImpl::add(int64_t a, int64_t b)\n   {\n       return calc_add(static_cast<int>(a), static_cast<int>(b));\n   }\n\n   int64_t CalcModuleImpl::multiply(int64_t a, int64_t b)\n   {\n       return calc_multiply(static_cast<int>(a), static_cast<int>(b));\n   }\n\n   int64_t CalcModuleImpl::factorial(int64_t n)\n   {\n       return calc_factorial(static_cast<int>(n));\n   }\n\n   int64_t CalcModuleImpl::fibonacci(int64_t n)\n   {\n       return calc_fibonacci(static_cast<int>(n));\n   }\n\n   std::string CalcModuleImpl::libVersion()\n   {\n       return std::string(calc_version());\n   }\n\n   void CalcModuleImpl::libVersionNotify()\n   {\n       // Emit the event declared in `logos_events:`. When the module is\n       // loaded by a host, this reaches every subscriber. When the class\n       // is constructed outside a host (e.g. in unit tests), it is a\n       // safe no-op.\n       versionReady(std::string(calc_version()));\n   }\n   ```\n\n   You don't write `initLogos`, `Q_INVOKABLE`, `name()`, `version()` or `lidl()`. The code generator produces them from the header and `metadata.json`.\n\n## Step 4: Build the module\n\n1. Create `.gitignore`, so build output stays out of the repository:\n\n   ```text\n   # Nix build output\n   result\n   result-*\n\n   # CMake build directory\n   build/\n   ```\n\n1. Initialise the Git repository, stage the files and lock the flake inputs:\n\n   ```bash\n   git init\n   git add -A\n   nix flake update\n   git add flake.lock\n   ```\n\n   - Nix only sees files that Git tracks.\n\n1. Build the plugin library:\n\n   ```bash\n   nix build '.#lib'\n   ```\n\n   - The first build takes 5 to 15 minutes while Nix downloads Qt, the Logos SDK and their dependencies. Later builds use the Nix cache.\n   - Keep the quotes around `'.#lib'`. Some shells, notably zsh, interpret `#` otherwise.\n\n1. Build the full [package](https://docs.logos.co/get-started/glossary.md#package):\n\n   ```bash\n   nix build\n   ```\n\n   - This build derives the module's LIDL contract from `src/calc_module_impl.h` and generates the Qt plugin from it before CMake compiles it.\n\n1. Inspect the output:\n\n   ```bash\n   ls -la result/lib/\n   ```\n\n   The plugin and the C library sit side by side, so the plugin finds the library at runtime through its RPATH:\n\n   ```text\n   # Linux\n   calc_module_plugin.so   # Your Logos module plugin\n   libcalc.so              # The C library (copied alongside)\n\n   # macOS\n   calc_module_plugin.dylib\n   libcalc.dylib\n   ```\n\n## Step 5: Inspect the module\n\nUse the [`lm`](https://docs.logos.co/get-started/glossary.md#lm) tool from `logos-module` to inspect the compiled plugin.\n\n1. Build `lm`:\n\n   ```bash\n   nix build 'github:logos-co/logos-module/0.3.0#lm' --out-link ./lm\n   ```\n\n1. View the module's metadata:\n\n   ```bash\n   # Linux\n   ./lm/bin/lm metadata result/lib/calc_module_plugin.so\n\n   # macOS\n   ./lm/bin/lm metadata result/lib/calc_module_plugin.dylib\n   ```\n\n   ```text\n   Plugin Metadata:\n   ================\n   Name:         calc_module\n   Display name: (unset — falls back to name)\n   Version:      1.0.0\n   Description:  Calculator module wrapping libcalc C library\n   Author:\n   Type:         core\n   Protocol:     0.9.0\n   Dependencies: (none)\n   ```\n\n1. List the module's methods:\n\n   ```bash\n   # Linux\n   ./lm/bin/lm methods result/lib/calc_module_plugin.so\n\n   # macOS\n   ./lm/bin/lm methods result/lib/calc_module_plugin.dylib\n   ```\n\n   ```text\n   Plugin Methods:\n   ===============\n\n   int add(int a, int b)\n     Signature: add(int,int)\n     Invokable: yes\n     Description: Adds two integers and returns the sum.\n\n   ...\n\n   int factorial(int n)\n     Signature: factorial(int)\n     Invokable: yes\n     Description:\n       Computes the factorial n! of a non-negative integer.\n       Defined as n * (n-1) * ... * 1, with 0! = 1.\n\n   ...\n\n   tstr lidl()\n     Signature: lidl()\n     Invokable: yes\n     Description: The module's canonical LIDL interface document.\n   ```\n\n   - Signatures use the contract types (`int`, `tstr`), not the C++ types you wrote. In LIDL, `int` is 64-bit, so a value that fits your `int64_t` is never truncated on the way across.\n   - Each `Description` is the method's doc comment, with its line breaks kept.\n   - `name()`, `version()` and `lidl()` are generated for every module. The last one returns the module's LIDL contract.\n   - Add `--json` for output that scripts and CI can parse.\n\n1. List the module's events:\n\n   ```bash\n   # Linux\n   ./lm/bin/lm events result/lib/calc_module_plugin.so\n\n   # macOS\n   ./lm/bin/lm events result/lib/calc_module_plugin.dylib\n   ```\n\n   ```text\n   Plugin Events:\n   ==============\n\n   void versionReady(tstr version)\n     Signature: versionReady(tstr)\n     Description:\n       Emitted by libVersionNotify() once the library version is known.\n       Carries the version string read from libcalc.\n   ```\n\n   Running `lm` with no subcommand prints the metadata, methods and events together.\n\n## Step 6: Install the module with `logosctl`\n\n`logosctl` installs modules from LGX packages, which carry the plugin, its libraries and a `manifest.json`. Build a portable package, start a daemon in a session of its own and install the package into it.\n\n1. Build the portable LGX package:\n\n   ```bash\n   nix build '.#lgx-portable' --out-link result-lgx-portable\n   ```\n\n   - Build `lgx-portable`, not `lgx`. The `lgx` output is a development build that keeps its libraries in the Nix store (variant `darwin-arm64-dev` on Apple Silicon, for example), and `logosctl` refuses to install it. See [Troubleshooting](#logosctl-cannot-find-the-module).\n\n1. Point `logosctl` at a session for this project. Run this in every terminal you use for the rest of this procedure:\n\n   ```bash\n   export LOGOSCTL_CONFIG_DIR=\"$PWD/session\"\n   ```\n\n   - A [session](https://github.com/logos-co/logos-logoscore-cli/blob/master/docs/logosctl.md#sessions) holds its own modules, logs and data, so packages from other projects stay out of it. Without `LOGOSCTL_CONFIG_DIR`, `logosctl` uses `~/.logosctl`.\n\n1. Start the daemon, detached so this terminal stays free:\n\n   ```bash\n   logosctl daemon start --detach\n   ```\n\n   - The command returns once the daemon accepts commands.\n\n1. Install the package:\n\n   ```bash\n   logosctl package install --file ./result-lgx-portable/*.lgx -y\n   ```\n\n   - `-y` applies the install without asking for confirmation.\n\n   The plugin, the C library and the manifest unpack into the session's `modules/` directory:\n\n   ```text\n   session/modules/calc_module/\n   ├── calc_module_plugin.dylib   # (or .so on Linux)\n   ├── libcalc.dylib              # (or .so on Linux)\n   ├── assets/lidl/               # The module's interface, in LIDL\n   ├── manifest.json              # Package manifest\n   ├── variant                    # Platform variant identifier\n   └── ...                        # Runtime libraries bundled by the portable build\n   ```\n\n1. Confirm that the package is installed:\n\n   ```bash\n   logosctl package ls\n   ```\n\n   `calc_module` is listed with the source `user`, next to the packages embedded in `logosctl`.\n\n## Step 7: Call the module from `logosctl`\n\n1. Load the module:\n\n   ```bash\n   logosctl module load calc_module\n   ```\n\n1. Inspect the module's methods and events:\n\n   ```bash\n   logosctl module show calc_module\n   ```\n\n   ```text\n   Name:          calc_module\n   Version:       v1.0.0\n   Status:        loaded\n   ...\n\n   Methods:\n     add(a: int, b: int) -> int\n         Adds two integers and returns the sum.\n     multiply(a: int, b: int) -> int\n         Multiplies two integers and returns the product.\n     factorial(n: int) -> int\n         Computes the factorial n! of a non-negative integer.\n         Defined as n * (n-1) * ... * 1, with 0! = 1.\n     ...\n\n   Events:\n     versionReady(version: tstr)\n         Emitted by libVersionNotify() once the library version is known.\n         Carries the version string read from libcalc.\n   ```\n\n   - The descriptions are the doc comments from `src/calc_module_impl.h`, the same ones `lm` showed.\n\n1. Call its methods:\n\n   ```bash\n   logosctl call calc_module add 3 5\n   logosctl call calc_module factorial 5\n   logosctl call calc_module fibonacci 10\n   logosctl call calc_module libVersion\n   ```\n\n   Each call prints its result:\n\n   ```text\n   8\n   120\n   55\n   1.0.0\n   ```\n\n   - The daemon writes its log, including the output of every module, to `session/logs/daemon.log`.\n\n1. Stop the daemon:\n\n   ```bash\n   logosctl daemon stop\n   ```\n\n## Step 8: Unit-test the module\n\nBecause the module is a plain C++ class, you can unit-test it directly, with no Qt and no daemon. The [Logos Test Framework](https://github.com/logos-co/logos-test-framework) provides a test runner (`LOGOS_TEST` and `LOGOS_ASSERT_*`) and link-time mocks of your C library, so each test decides what the C functions return and checks how the wrapper behaves.\n\n1. Enable tests in `flake.nix` by adding a `tests` block to the `mkLogosModule` call. `mockCLibs` lists the external libraries to replace with link-time mocks, so the tests don't link the real `libcalc`:\n\n   ```nix\n   {\n     description = \"Calculator module - wraps libcalc C library for Logos\";\n\n     inputs = {\n       logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.2\";\n     };\n\n     outputs = inputs@{ logos-module-builder, ... }:\n       logos-module-builder.lib.mkLogosModule {\n         src = ./.;\n         configFile = ./metadata.json;\n         flakeInputs = inputs;\n         tests = {\n           dir = ./tests;\n           mockCLibs = [ \"calc\" ];\n         };\n       };\n   }\n   ```\n\n1. Create `tests/CMakeLists.txt`. The test build configures `tests/` as its own CMake project, which calls `logos_test()` from the framework:\n\n   ```cmake\n   cmake_minimum_required(VERSION 3.14)\n   project(CalcModuleTests LANGUAGES CXX)\n\n   include(LogosTest)\n\n   logos_test(\n       NAME calc_module_tests\n       MODULE_SOURCES\n           ../src/calc_module_impl.cpp\n           mocks/calc_module_events_stub.cpp\n       TEST_SOURCES\n           main.cpp\n           test_calc.cpp\n       MOCK_C_SOURCES\n           mocks/mock_libcalc.cpp\n   )\n   ```\n\n   - `MODULE_SOURCES`: your implementation, compiled into the test binary, and the events stub from the next action.\n   - `TEST_SOURCES`: the runner entry point and your test files.\n   - `MOCK_C_SOURCES`: the link-time replacement for `libcalc`.\n   - `logos_test()` puts the repository root and `../src` on the include path, so `#include \"calc_module_impl.h\"` and `#include \"lib/libcalc.h\"` both resolve.\n\n1. Create `tests/mocks/calc_module_events_stub.cpp`. In a normal build, the code generator writes the body of every method in `logos_events:`. The test build skips that step, so `libVersionNotify()` needs a no-op body for `versionReady` to link:\n\n   ```cpp\n   // Stub bodies for the impl's `logos_events:` methods.\n   // In the real build the codegen generates calc_module_events.cpp with\n   // bodies that route through LogosModuleContext. The test build skips\n   // that codegen, so we provide no-op stubs to satisfy the linker.\n   #include \"calc_module_impl.h\"\n\n   void CalcModuleImpl::versionReady(const std::string&) {}\n   ```\n\n   - Add a matching no-op line for each event you add to `logos_events:`. A module without events doesn't need this file.\n\n1. Create `tests/main.cpp`, which pulls in the framework's `main()`:\n\n   ```cpp\n   #include <logos_test.h>\n\n   LOGOS_TEST_MAIN()\n   ```\n\n1. Create `tests/mocks/mock_libcalc.cpp`. Each function has the signature of a `libcalc` function, records the call and returns the value the test set:\n\n   ```cpp\n   // Link-time replacement for libcalc. Each function records the call\n   // and returns whatever the active test configured via mockCFunction().\n   #include <logos_clib_mock.h>\n\n   extern \"C\" {\n       #include \"lib/libcalc.h\"\n   }\n\n   extern \"C\" int calc_add(int a, int b) {\n       LOGOS_CMOCK_RECORD(\"calc_add\");\n       return LOGOS_CMOCK_RETURN(int, \"calc_add\");\n   }\n\n   extern \"C\" int calc_multiply(int a, int b) {\n       LOGOS_CMOCK_RECORD(\"calc_multiply\");\n       return LOGOS_CMOCK_RETURN(int, \"calc_multiply\");\n   }\n\n   extern \"C\" int calc_factorial(int n) {\n       LOGOS_CMOCK_RECORD(\"calc_factorial\");\n       return LOGOS_CMOCK_RETURN(int, \"calc_factorial\");\n   }\n\n   extern \"C\" int calc_fibonacci(int n) {\n       LOGOS_CMOCK_RECORD(\"calc_fibonacci\");\n       return LOGOS_CMOCK_RETURN(int, \"calc_fibonacci\");\n   }\n\n   extern \"C\" const char* calc_version(void) {\n       LOGOS_CMOCK_RECORD(\"calc_version\");\n       return LOGOS_CMOCK_RETURN_STRING(\"calc_version\");\n   }\n   ```\n\n1. Create `tests/test_calc.cpp`. Each test constructs `CalcModuleImpl` directly, sets what the C functions return, calls a method and asserts on the result. `LogosTestContext` resets the mocks between tests:\n\n   ```cpp\n   #include <logos_test.h>\n   #include \"calc_module_impl.h\"\n\n   LOGOS_TEST(add_forwards_to_calc_add) {\n       auto t = LogosTestContext(\"calc_module\");\n       t.mockCFunction(\"calc_add\").returns(8);\n\n       CalcModuleImpl calc;\n       LOGOS_ASSERT_EQ(calc.add(3, 5), 8);\n       LOGOS_ASSERT(t.cFunctionCalled(\"calc_add\"));\n   }\n\n   LOGOS_TEST(multiply_forwards_to_calc_multiply) {\n       auto t = LogosTestContext(\"calc_module\");\n       t.mockCFunction(\"calc_multiply\").returns(42);\n\n       CalcModuleImpl calc;\n       LOGOS_ASSERT_EQ(calc.multiply(6, 7), 42);\n       LOGOS_ASSERT(t.cFunctionCalled(\"calc_multiply\"));\n   }\n\n   LOGOS_TEST(factorial_returns_mocked_value) {\n       auto t = LogosTestContext(\"calc_module\");\n       t.mockCFunction(\"calc_factorial\").returns(120);\n\n       CalcModuleImpl calc;\n       LOGOS_ASSERT_EQ(calc.factorial(5), 120);\n   }\n\n   LOGOS_TEST(libVersion_converts_cstring_to_string) {\n       auto t = LogosTestContext(\"calc_module\");\n       t.mockCFunction(\"calc_version\").returns(\"1.0.0\");\n\n       CalcModuleImpl calc;\n       LOGOS_ASSERT_EQ(calc.libVersion(), std::string(\"1.0.0\"));\n   }\n   ```\n\n   - `<logos_test.h>` also provides `LOGOS_ASSERT_TRUE`, `LOGOS_ASSERT_FALSE`, `LOGOS_ASSERT_NE`, `LOGOS_ASSERT_GT`, `LOGOS_ASSERT_GE` and `LOGOS_ASSERT_LT`.\n\n1. Track the new files, then build and run the tests:\n\n   ```bash\n   git add tests/ flake.nix\n   nix build '.#unit-tests' -L\n   ```\n\n   The build compiles the implementation against the mocks and runs every test. A passing run ends with a summary:\n\n   ```text\n   logos-calc_module-tests>    PASS  add_forwards_to_calc_add  0ms\n   logos-calc_module-tests>    PASS  multiply_forwards_to_calc_multiply  0ms\n   logos-calc_module-tests>    PASS  factorial_returns_mocked_value  0ms\n   logos-calc_module-tests>    PASS  libVersion_converts_cstring_to_string  0ms\n   logos-calc_module-tests>\n   logos-calc_module-tests>  ── Results: 4 passed (0ms) ──────\n   ```\n\n   - A failed assertion prints its file and line and fails the build.\n\n## Troubleshooting Logos module wrapping\n\n### A method doesn't appear in `lm` or can't be called\n\nThe code generator exposes only `public` methods whose parameter and return types it recognises. Check that the method is in the `public:` section, that it uses only the types in the table in [Step 3](#step-3-configure-the-logos-module) (notably `int64_t`, not `int`, and `std::string`, not `char*` or `QString`), and that each method is declared on its own.\n\n### The build fails with an unknown type or a method the generator can't parse\n\nThe generator reads `src/calc_module_impl.h` as text to derive the module's contract. Qt types or unusual templates in a public method signature confuse it. Keep Qt out of the header entirely, and move helpers that need other types into the `private:` section or the `.cpp` file.\n\n### The plugin fails with `Cannot load library`\n\n```text\nCannot load library calc_module_plugin.so: libcalc.so: cannot open shared object file\n```\n\nThe C library must sit in the same directory as the plugin. The build sets the plugin's RPATH to `$ORIGIN` on Linux and `@loader_path` on macOS, so it looks for libraries in its own directory.\n\n### Events never reach subscribers\n\nCheck that the event is declared in a `logos_events:` section and that the class inherits `LogosModuleContext`. Events fire only when a host such as `logosctl` or Basecamp loads the module; constructed directly, as in unit tests, emitting is a no-op. Subscribers must use the exact event name, for example `logos.onModuleEvent(\"calc_module\", \"versionReady\")`.\n\n### `logosctl` cannot find the module\n\nCheck that:\n\n- The module is installed in the current session: `logosctl package ls` lists it with the source `user`. If you opened a new terminal, export `LOGOSCTL_CONFIG_DIR` again.\n- The install did not stop at the variant check. `logosctl` installs only the portable variant for its own platform. A package from `.#lgx` fails with `Package does not contain variant for platform: … (package provides: …-dev)`, so build `.#lgx-portable`.\n- You load the module by the `name` in `metadata.json` (`calc_module`), not by the package file name.\n\n### `nix build .#lib` does nothing or fails silently\n\nSome shells, notably zsh, treat `#` specially. Put the flake reference in quotes: `nix build '.#lib'`.\n\n### The first build is slow\n\nThe first `nix build` downloads Qt 6, the Logos C++ SDK, the code generator and their dependencies. This is a one-time cost; later builds use the Nix cache.\n\n### The build fails with undefined symbols from the C library\n\nCheck that the `.so` or `.dylib` is in `lib/` before you build, that the header has `extern \"C\"` guards, and that the symbols are exported: `nm -D lib/libcalc.so | grep calc` on Linux, or `nm -gU lib/libcalc.dylib | grep calc` on macOS.\n"},{"id":"core/build-modules/build-a-logos-cpp-ui-module","title":"Build a Logos C++ UI module","summary":"Get started building a ui\\_qml module with a C++ backend that runs in a separate process.","section":"core","doc_type":"procedure","url":"https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module","markdown_url":"https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module.md","permalink":"/core/build-modules/build-a-logos-cpp-ui-module","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/build-modules/build-a-logos-cpp-ui-module.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Scaffold the project"},{"depth":2,"text":"Step 2: Configure the module metadata"},{"depth":2,"text":"Step 3: Define the remote interface"},{"depth":2,"text":"Step 4: Configure the CMake build"},{"depth":2,"text":"Step 5: Write the C++ backend"},{"depth":2,"text":"Step 6: Write the QML view"},{"depth":2,"text":"Step 7: Use the Logos Design System in your QML (optional)"},{"depth":2,"text":"Step 8: Configure the Nix flake"},{"depth":2,"text":"Step 9: Build and run the module"},{"depth":2,"text":"Step 10: Reload the view as you edit it (optional)"},{"depth":2,"text":"Step 11: Add UI integration tests (optional)"},{"depth":2,"text":"Troubleshooting the C++ UI module build"},{"depth":3,"text":"Linker errors during `nix build`"},{"depth":3,"text":"The development launcher doesn't find your QML"},{"depth":3,"text":"Known constraints"}],"markdown":"# Build a Logos C++ UI module\n\n#### Get started building a ui\\_qml module with a C++ backend that runs in a separate process.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis guide covers building a [UI module](https://docs.logos.co/get-started/glossary.md#ui-module) that pairs a QML view with a C++ backend. The backend runs in a separate `ui-host` process while the view loads inside the host app ([`logos-standalone-app`](https://docs.logos.co/get-started/glossary.md#logos-standalone-app) or Logos Basecamp), so a backend crash cannot bring down the host. The backend calls the `calc_module` from [Wrap a C library as a Logos module](https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md) through a typed, generated API.\n\nYou write two things: a `.rep` file, the contract between the view and the backend, and a C++ backend class that implements it. The build generates the Qt plugin and interface classes around them. This guide is intended for developers who want typed, process-isolated calls from their UI to other modules.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux x86_64 or aarch64\n>    - macOS arm64 (Apple Silicon)\n> - The `logos-calc-module` project from [Wrap a C library as a Logos module](https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md), with its shared library built in `logos-calc-module/lib/`. You create this guide's project next to it.\n> - Basic familiarity with [QML](https://doc.qt.io/qt-6/qmlapplications.html).\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n\n## What to expect\n\n- You can build a `calc_ui_cpp` module whose C++ backend runs in its own process, and run it in `logos-standalone-app` with `nix run`.\n- You can connect a QML view to the backend's slots, properties and signals, and to an event that `calc_module` emits.\n- You can reload the view as you edit it, and check it with automated UI tests.\n\n## Step 1: Scaffold the project\n\n1. Create the project directory next to `logos-calc-module`, and enter it:\n\n   ```bash\n   mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp\n   ```\n\n1. Initialise the project from the C++ backend UI template:\n\n   ```bash\n   nix flake init -t github:logos-co/logos-module-builder/0.3.2#ui-qml-backend\n   ```\n\n   - The template follows the universal authoring model: `metadata.json` sets `\"interface\": \"universal\"`, and the only sources are an example `.rep` file and one backend class. The plugin and interface classes are generated.\n\n1. Remove the template's example `.rep` file and backend, which the following steps replace:\n\n   ```bash\n   rm -f src/ui_example.rep src/ui_example_backend.h src/ui_example_backend.cpp\n   ```\n\n1. Initialise a Git repository and stage the files:\n\n   ```bash\n   git init && git add -A\n   ```\n\n## Step 2: Configure the module metadata\n\n1. Replace `metadata.json` with the following:\n\n   ```json\n   {\n     \"name\": \"calc_ui_cpp\",\n     \"version\": \"1.0.0\",\n     \"type\": \"ui_qml\",\n     \"interface\": \"universal\",\n     \"category\": \"tools\",\n     \"description\": \"Calculator C++ UI — QML view with process-isolated backend for calc_module\",\n     \"main\": \"calc_ui_cpp_plugin\",\n     \"view\": \"qml/Main.qml\",\n     \"icon\": \"icons/calc.png\",\n     \"dependencies\": [\"calc_module\"],\n     \"codegen\": { \"rep\": \"src/calc_ui_cpp.rep\" },\n\n     \"nix\": {\n       \"packages\": {\n         \"build\": [],\n         \"runtime\": []\n       },\n       \"external_libraries\": [],\n       \"cmake\": {\n         \"find_packages\": [],\n         \"extra_sources\": [],\n         \"extra_include_dirs\": [],\n         \"extra_link_libraries\": []\n       }\n     }\n   }\n   ```\n\n   | Field | What it does |\n   | --- | --- |\n   | `type` | `\"ui_qml\"` marks a module with a QML view. |\n   | `interface` | `\"universal\"` selects the universal authoring model: you write the `.rep` file and a backend class, and the build generates the plugin around them. Without it, the build expects a hand-written plugin. |\n   | `codegen.rep` | Your `.rep` file. `backend_class` and `backend_header` can be set too; they default to `CalcUiCppBackend` and `calc_ui_cpp_backend.h`. |\n   | `main` | The name of the generated backend plugin library, without its extension. |\n   | `view` | The QML entry point. |\n   | `dependencies` | The core modules that the backend calls. Each one is also a flake input of the same name, set up in [Step 8](#step-8-configure-the-nix-flake). |\n\n1. Create the icon that the Logos Basecamp sidebar shows for the module. LGX packaging accepts only a 256×256 PNG, and this command writes a placeholder of that size:\n\n   ```bash\n   mkdir -p icons\n   # Copy any PNG here — or generate a 256×256 placeholder:\n   echo \"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUuzHEuzHEVOa2oAAAAH0lEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAvg0hAAABYOSdlwAAAABJRU5ErkJggg==\" | base64 -d > icons/calc.png\n   ```\n\n## Step 3: Define the remote interface\n\nThe `.rep` file is the contract between the QML view and the C++ backend, and the one Qt-typed file you write. At build time, Qt's `repc` compiler generates `CalcUiCppSimpleSource`, the base class that your backend implements, and `CalcUiCppReplica`, the typed replica that the view uses.\n\n1. Create `src/calc_ui_cpp.rep`:\n\n   ```rep\n   class CalcUiCpp\n   {\n       // ── SLOTs — call-and-return; each reply reaches QML via logos.watch() ──\n       SLOT(int add(int a, int b))\n       SLOT(int multiply(int a, int b))\n       SLOT(int factorial(int n))\n       SLOT(int fibonacci(int n))\n       SLOT(QString libVersion())\n\n       // Void slot — fire-and-forget. Asks calc_module to (re-)announce\n       // its version as a `versionReady` event; there's no return value\n       // to await, the answer comes back through the PROP below.\n       SLOT(void announceVersion())\n\n       // ── PROPs — auto-synced backend → every QML replica, no polling ──\n       // QString, event-fed: the typed `versionReady` subscription the\n       // backend arms in onContextReady() writes it. Starts empty.\n       PROP(QString versionEvent=\"\" READONLY)\n\n       // int, slot-driven: the backend bumps it after each calculation,\n       // so the view shows a live tally without ever polling.\n       PROP(int computeCount=0 READONLY)\n\n       // int, READWRITE: a memory register the QML view both *reads* and\n       // *writes* (Store / Clear buttons), and the backend may set too.\n       // A write round-trips QML → replica → source → back to every replica.\n       PROP(int memory=0 READWRITE)\n\n       // ── SIGNAL — backend → view push, distinct from a return value ──\n       // Emitted after each calculation. QML catches it with a\n       // Connections block (not logos.watch(), not a PROP read).\n       SIGNAL(computed(QString op, int result))\n   }\n   ```\n\n   Each kind of member reaches the view in its own way:\n\n   | Member | Backend | QML view |\n   | --- | --- | --- |\n   | `SLOT` with a return value, such as `add` | Overrides `int add(int a, int b)` | `logos.watch(backend.add(1, 2), ...)` delivers the reply |\n   | `SLOT` returning `void`, such as `announceVersion` | Overrides `void announceVersion()` | Calls `backend.announceVersion()`, with no reply to wait for |\n   | `READONLY` `PROP`, such as `computeCount` | Writes it with the generated `setComputeCount()` | Reads `backend.computeCount`, which updates without polling |\n   | `READWRITE` `PROP`, such as `memory` | Can write it with `setMemory()` | Reads `backend.memory` and can assign it, for example `backend.memory = 8` |\n   | `SIGNAL`, such as `computed` | Emits it with `emit computed(op, result)` | Handles it in a `Connections` block, in `onComputed` |\n\n## Step 4: Configure the CMake build\n\n1. Replace `CMakeLists.txt` with the following:\n\n   ```cmake\n   cmake_minimum_required(VERSION 3.14)\n   project(CalcUiCppPlugin LANGUAGES CXX)\n\n   if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})\n       include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)\n   else()\n       message(FATAL_ERROR \"LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.\")\n   endif()\n\n   # Derive the module name from metadata.json — single source of truth.\n   file(READ \"${CMAKE_CURRENT_SOURCE_DIR}/metadata.json\" METADATA_JSON)\n   string(JSON MODULE_NAME GET ${METADATA_JSON} name)\n\n   logos_module(\n       NAME ${MODULE_NAME}\n       REP_FILE src/calc_ui_cpp.rep\n       SOURCES\n           src/calc_ui_cpp_backend.h\n           src/calc_ui_cpp_backend.cpp\n       INCLUDE_DIRS\n           src\n   )\n   ```\n\n   - List only your two source files. `REP_FILE` tells `logos_module()` to run `repc`, generate the plugin and interface classes around your backend, and build a separate `calc_ui_cpp_replica_factory` library that the host uses to create typed replicas.\n\n## Step 5: Write the C++ backend\n\nThe backend is the only C++ that you write. It derives `CalcUiCppSimpleSource`, generated from the `.rep` file, and `LogosUiPluginContext`, which provides `modules()`: typed calls and event subscriptions for each module in `dependencies`.\n\n1. Create `src/calc_ui_cpp_backend.h`:\n\n   ```cpp\n   #pragma once\n\n   #include \"rep_calc_ui_cpp_source.h\"\n   #include \"logos_ui_plugin_context.h\"\n\n   // The whole hand-written backend. Derives:\n   //   - CalcUiCppSimpleSource — generated from calc_ui_cpp.rep; override its\n   //     slots (the QML replica gets each return value via Qt Remote Objects).\n   //   - LogosUiPluginContext — supplies modules() (Qt-typed callers + typed event\n   //     subscriptions for \"dependencies\") and onContextReady(). A UI plugin is a\n   //     view, not a module, so that is all the context carries.\n   // The *Plugin / *Interface classes (Q_PLUGIN_METADATA, initLogos wiring,\n   // QtRO registration) are generated around it.\n   class CalcUiCppBackend : public CalcUiCppSimpleSource,\n                            public LogosUiPluginContext\n   {\n   public:\n       // Slots from calc_ui_cpp.rep — each delegates to calc_module.\n       int add(int a, int b) override;\n       int multiply(int a, int b) override;\n       int factorial(int n) override;\n       int fibonacci(int n) override;\n       QString libVersion() override;\n\n       // Tells calc_module to emit its `versionReady` event.\n       void announceVersion() override;\n\n       // Fires once when ui-host hands the plugin its LogosAPI — the\n       // typed dependency surface is live, so we arm the event\n       // subscription here (before the view's first call).\n       void onContextReady() override;\n\n   private:\n       // Feeds the non-slot surfaces of the .rep after each calculation:\n       // bumps the computeCount PROP (setComputeCount, generated) and\n       // emits the `computed` SIGNAL. The READWRITE `memory` PROP is\n       // driven from QML, so the backend doesn't have to touch it.\n       void record(const QString& op, int result);\n   };\n   ```\n\n   - There is no `Q_OBJECT`, `Q_PLUGIN_METADATA`, `initLogos`, `name()` or `version()` to write. The build generates them.\n\n1. Create `src/calc_ui_cpp_backend.cpp`:\n\n   ```cpp\n   #include \"calc_ui_cpp_backend.h\"\n\n   // Generated umbrella: LogosModules (behind modules()) from\n   // metadata.json#dependencies — typed wrappers + typed event accessors.\n   #include \"logos_sdk.h\"\n\n   int CalcUiCppBackend::add(int a, int b)\n   {\n       int result = modules().calc_module.add(a, b);\n       record(\"add\", result);\n       return result;\n   }\n\n   int CalcUiCppBackend::multiply(int a, int b)\n   {\n       int result = modules().calc_module.multiply(a, b);\n       record(\"multiply\", result);\n       return result;\n   }\n\n   int CalcUiCppBackend::factorial(int n)\n   {\n       int result = modules().calc_module.factorial(n);\n       record(\"factorial\", result);\n       return result;\n   }\n\n   int CalcUiCppBackend::fibonacci(int n)\n   {\n       int result = modules().calc_module.fibonacci(n);\n       record(\"fibonacci\", result);\n       return result;\n   }\n\n   QString CalcUiCppBackend::libVersion()\n   {\n       // A UI plugin is Qt-typed: modules().calc_module's wrapper returns QString\n       // (api-style qt), matching the .rep slot — no conversion needed.\n       return modules().calc_module.libVersion();\n   }\n\n   void CalcUiCppBackend::record(const QString& op, int result)\n   {\n       // PROP: bump the slot-driven counter. setComputeCount() is the\n       // generated setter; Qt Remote Objects syncs the new value to every\n       // replica, so the view's \"Computations\" label updates with no polling.\n       setComputeCount(computeCount() + 1);\n\n       // SIGNAL: a backend → view push, distinct from the return value the\n       // QML side gets via logos.watch(). `computed` is declared on the\n       // generated SimpleSource, so we just emit it; the typed replica\n       // re-emits it and the view's Connections block catches it.\n       emit computed(op, result);\n   }\n\n   void CalcUiCppBackend::announceVersion()\n   {\n       // Fire-and-forget call into calc_module: it looks up the library\n       // version and emits it as a `versionReady` event. We don't read a\n       // return value here — the event comes back through the subscription\n       // armed in onContextReady() below.\n       modules().calc_module.libVersionNotify();\n   }\n\n   void CalcUiCppBackend::onContextReady()\n   {\n       // Typed module-event subscription. `versionReady` is calc_module's\n       // event (Part 1's `logos_events:` block); the generated wrapper\n       // exposes it as on<Event> + a Qt-typed callback (QString, because a\n       // UI plugin is api-style qt). Push each payload into the versionEvent\n       // PROP — Qt Remote Objects then auto-syncs it to the QML replica.\n       modules().calc_module.onVersionReady([this](const QString& version) {\n           setVersionEvent(version);\n       });\n   }\n   ```\n\n   - `modules().calc_module.add(a, b)` calls `calc_module` through the generated, typed API. A UI plugin gets Qt types (`int`, `QString`) that match its `.rep` slots, even where `calc_module` declares `std::string`; the generated wrapper converts them.\n   - `record()` updates the `computeCount` property and emits the `computed` signal after each calculation.\n   - `onContextReady()` subscribes to the `versionReady` event of `calc_module` and writes each payload to the `versionEvent` property. Subscribe there, not in the constructor: `modules()` is ready only once the framework calls `onContextReady()`.\n\n## Step 6: Write the QML view\n\n1. Create `src/qml/Main.qml`:\n\n   ```qml\n   import QtQuick\n   import QtQuick.Controls\n   import QtQuick.Layouts\n\n   Item {\n       id: root\n\n       property string result: \"\"\n       property string errorText: \"\"\n\n       // Last payload from the backend's `computed` SIGNAL (see Connections below).\n       property string lastSignal: \"(none)\"\n\n       // Typed replica of the backend running in ui-host (generated from calc_ui_cpp.rep).\n       readonly property var backend: logos.module(\"calc_ui_cpp\")\n\n       // The ui-host backend connects asynchronously, so the replica isn't\n       // immediately usable. Track readiness reactively: isViewModuleReady()\n       // is a Q_INVOKABLE (not a property), so we re-check it on the\n       // onViewModuleReadyChanged signal and once at startup — never via a\n       // plain property binding, which would not re-evaluate.\n       property bool ready: false\n\n       Connections {\n           target: logos\n           function onViewModuleReadyChanged(moduleName, isReady) {\n               if (moduleName === \"calc_ui_cpp\")\n                   root.ready = isReady && root.backend !== null\n           }\n       }\n       Component.onCompleted: {\n           root.ready = root.backend !== null && logos.isViewModuleReady(\"calc_ui_cpp\")\n       }\n\n       // SIGNAL from the .rep: the backend emits `computed(op, result)` after\n       // each calculation. The typed replica re-emits it, so we catch it with\n       // a Connections block — no logos.watch(), no property read. This is the\n       // backend → view push path, distinct from the slot return value above.\n       Connections {\n           target: root.backend\n           function onComputed(op, result) {\n               root.lastSignal = op + \" = \" + result\n           }\n       }\n\n       // logos.watch() delivers the result of a replica slot call via callbacks.\n       // No QtRemoteObjects import needed — the bridge handles it.\n       function callCalc(method, args) {\n           if (!root.ready) {\n               root.errorText = \"Backend not ready\"\n               return\n           }\n           root.errorText = \"\"\n           root.result = \"...\"\n           logos.watch(backend[method].apply(backend, args),\n               function(value) { root.result = String(value) },\n               function(error) { root.errorText = String(error) }\n           )\n       }\n\n       ColumnLayout {\n           anchors.fill: parent\n           anchors.margins: 24\n           spacing: 16\n\n           Text {\n               text: \"Logos Calculator (C++ backend)\"\n               font.pixelSize: 20\n               color: \"#ffffff\"\n               Layout.alignment: Qt.AlignHCenter\n           }\n\n           // Reactive backend-connection indicator.\n           Text {\n               text: root.ready ? \"Connected\" : \"Connecting to backend...\"\n               color: root.ready ? \"#56d364\" : \"#f0883e\"\n               font.pixelSize: 12\n               Layout.alignment: Qt.AlignHCenter\n           }\n\n           RowLayout {\n               spacing: 12\n               Layout.fillWidth: true\n\n               TextField {\n                   id: inputA\n                   placeholderText: \"a\"\n                   Layout.preferredWidth: 80\n                   validator: IntValidator {}\n               }\n\n               TextField {\n                   id: inputB\n                   placeholderText: \"b\"\n                   Layout.preferredWidth: 80\n                   validator: IntValidator {}\n               }\n\n               Button {\n                   text: \"Add\"\n                   enabled: root.ready\n                   onClicked: root.callCalc(\"add\", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])\n               }\n\n               Button {\n                   text: \"Multiply\"\n                   enabled: root.ready\n                   onClicked: root.callCalc(\"multiply\", [parseInt(inputA.text) || 0, parseInt(inputB.text) || 0])\n               }\n           }\n\n           RowLayout {\n               spacing: 12\n               Layout.fillWidth: true\n\n               TextField {\n                   id: inputN\n                   placeholderText: \"n\"\n                   Layout.preferredWidth: 80\n                   validator: IntValidator { bottom: 0 }\n               }\n\n               Button {\n                   text: \"Factorial\"\n                   enabled: root.ready\n                   onClicked: root.callCalc(\"factorial\", [parseInt(inputN.text) || 0])\n               }\n\n               Button {\n                   text: \"Fibonacci\"\n                   enabled: root.ready\n                   onClicked: root.callCalc(\"fibonacci\", [parseInt(inputN.text) || 0])\n               }\n\n               Button {\n                   text: \"libcalc version\"\n                   enabled: root.ready\n                   onClicked: root.callCalc(\"libVersion\", [])\n               }\n\n               Button {\n                   // Fires the event path: asks calc_module to emit\n                   // versionReady. No logos.watch() — the result comes\n                   // back through the versionEvent PROP, not a return value.\n                   text: \"Announce version (event)\"\n                   enabled: root.ready\n                   onClicked: root.backend.announceVersion()\n               }\n           }\n\n           Rectangle {\n               Layout.fillWidth: true\n               height: 56\n               color: root.errorText.length > 0 ? \"#3d1a1a\" : \"#1a2d1a\"\n               radius: 8\n\n               Text {\n                   anchors.centerIn: parent\n                   text: root.errorText.length > 0 ? root.errorText\n                           : (root.result.length > 0 ? root.result : \"Enter values and press a button\")\n                   color: root.errorText.length > 0 ? \"#f85149\" : \"#56d364\"\n                   font.pixelSize: 15\n               }\n           }\n\n           // Slot-driven PROP: bumped by the backend's record() after each\n           // calculation. A plain property read — auto-syncs, no polling.\n           Text {\n               text: \"Computations: \" + ((root.ready && root.backend) ? root.backend.computeCount : 0)\n               color: \"#cdd6f4\"\n               font.pixelSize: 14\n               Layout.alignment: Qt.AlignHCenter\n           }\n\n           // SIGNAL payload, captured by the Connections block above.\n           Text {\n               text: \"Last op (signal): \" + root.lastSignal\n               color: \"#94e2d5\"\n               font.pixelSize: 14\n               Layout.alignment: Qt.AlignHCenter\n           }\n\n           // READWRITE PROP: the memory register. The label *reads*\n           // backend.memory; the buttons *write* it. A write round-trips\n           // QML → replica → backend source → back to every replica, so the\n           // label updates once the new value syncs home.\n           RowLayout {\n               spacing: 12\n               Layout.alignment: Qt.AlignHCenter\n\n               Text {\n                   text: \"Memory: \" + ((root.ready && root.backend) ? root.backend.memory : 0)\n                   color: \"#cdd6f4\"\n                   font.pixelSize: 14\n               }\n\n               Button {\n                   text: \"Store (MS)\"\n                   enabled: root.ready\n                   onClicked: root.backend.memory = parseInt(root.result) || 0\n               }\n\n               Button {\n                   text: \"Clear (MC)\"\n                   enabled: root.ready\n                   onClicked: root.backend.memory = 0\n               }\n           }\n\n           // Event-fed label: the versionEvent PROP auto-syncs from the\n           // backend's typed versionReady subscription. No polling — it\n           // updates the moment calc_module emits.\n           Text {\n               readonly property string ev: (root.ready && root.backend) ? root.backend.versionEvent : \"\"\n               text: \"Version event: \" + (ev.length > 0 ? ev : \"(none yet)\")\n               color: \"#f9e2af\"\n               font.pixelSize: 15\n               Layout.alignment: Qt.AlignHCenter\n           }\n\n           Item { Layout.fillHeight: true }\n       }\n   }\n   ```\n\n   - `logos.module(\"calc_ui_cpp\")` returns the typed replica of the backend. The host injects the `logos` object, so no `QtRemoteObjects` import is needed.\n   - The backend connects asynchronously from its own process. `logos.isViewModuleReady()` is a method, not a property, so the view checks it once at startup and again in `onViewModuleReadyChanged`, and enables the buttons only when the backend is ready.\n   - Slot replies arrive through `logos.watch()`, properties are read and written directly, and a `Connections` block on the backend handles the `computed` signal.\n\n## Step 7: Use the Logos Design System in your QML (optional)\n\nThe host app has [`logos-design-system`](https://github.com/logos-co/logos-design-system) on its QML import path. Use its themed components instead of raw controls and hard-coded colours, so the module keeps the Logos look as the design system evolves.\n\n1. In `src/qml/Main.qml`, import the design system and replace raw controls with its components, for example:\n\n   ```qml\n   import Logos.Theme\n   import Logos.Controls\n   import Logos.Icons        // optional shared icon assets\n\n   LogosButton {\n       text: qsTr(\"Add\")\n       onClicked: root.callCalc(\"add\", [parseInt(inputA.text) || 0,\n                                        parseInt(inputB.text) || 0])\n   }\n\n   LogosTextField {\n       id: inputA\n       placeholderText: qsTr(\"a\")\n   }\n\n   Rectangle {\n       color: Theme.palette.backgroundSecondary\n       radius: Theme.spacing.radiusSmall\n       LogosText { text: qsTr(\"Result\"); color: Theme.palette.text }\n   }\n   ```\n\n1. Build the design system's storybook, then run it to browse the available components:\n\n   ```bash\n   nix build github:logos-co/logos-design-system#bin-bundle-dir --out-link ../logos-storybook\n   ../logos-storybook/bin/LogosStorybook\n   ```\n\n   - **Controls** are designed and ready for production, for example `LogosButton`, `LogosBadge`, `LogosCheckbox`, `LogosComboBox`, `LogosIconButton`, `LogosPaginator`, `LogosSearchBar`, `LogosTabBar`, `LogosTable`, `LogosText`, `LogosTextField` and `LogosToolTip`.\n   - **Controls (not designed)** have stable APIs but unstyled visuals, for example `LogosDialog`, `LogosDrawer`, `LogosScrollView`, `LogosSpinner`, `LogosTextArea` and `LogosSwitch`. You can ship with them; they get the designed look later without changes to your QML.\n\n   Use theme tokens instead of colour literals and font sizes:\n\n   | Token | Values |\n   | --- | --- |\n   | `Theme.palette.*` | `background`, `backgroundSecondary`, `surface`, `text`, `textSecondary`, `border`, `primary`, `success`, `warning`, `error`, `info`, `hover`, `pressed`, … |\n   | `Theme.spacing.*` | `tiny`, `small`, `medium`, `large`, `xlarge`, `xxlarge`, `radiusSmall`, `radiusMedium`, `radiusLarge` |\n   | `Theme.typography.*` | `pageTitleText` (36), `titleText` (30), `panelTitleText` (24), `subtitleText` (16), `primaryText` (14), `secondaryText` (12), `weightRegular`, `weightMedium`, `weightBold`, `publicSans` |\n   | `LogosIcons.*` (from `Logos.Icons`) | `arrowLeft`, `arrowRight`, `refresh`, `install`, `trash`, `more`, `search`, … |\n\n## Step 8: Configure the Nix flake\n\n1. Replace `flake.nix` with the following:\n\n   ```nix\n   {\n     description = \"Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module\";\n\n     inputs = {\n       logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.2\";\n\n       # Points at your local calc_module checkout. This is a placeholder —\n       # you lock it to your actual path in the next step with\n       # `nix flake update --override-input` (see \"Lock and build\" below).\n       calc_module.url = \"path:/path/to/your/calc_module\";\n     };\n\n     outputs = inputs@{ logos-module-builder, calc_module, ... }:\n       logos-module-builder.lib.mkLogosQmlModule {\n         src = ./.;\n         configFile = ./metadata.json;\n         flakeInputs = inputs;\n       };\n   }\n   ```\n\n   - The `calc_module` input has the same name as the dependency in `metadata.json`.\n   - Leave the placeholder `path:/path/to/your/calc_module` as it is. Nix doesn't accept a relative path such as `../logos-calc-module` in `flake.nix`, so [Step 9](#step-9-build-and-run-the-module) records the real path in `flake.lock` instead.\n   - To fetch `calc_module` from a repository instead, use a `github:` URL, for example `calc_module.url = \"github:<your-org>/<your-calc-module>\";`.\n   - `mkLogosQmlModule` compiles the backend, bundles the view, builds the LGX [packages](https://docs.logos.co/get-started/glossary.md#package) and sets up `nix run`.\n\n## Step 9: Build and run the module\n\n1. Confirm that the shared library of `calc_module` exists:\n\n   ```bash\n   # Linux\n   ls ../logos-calc-module/lib/libcalc.so\n\n   # macOS\n   ls ../logos-calc-module/lib/libcalc.dylib\n   ```\n\n   - If it is missing, build it as in [Wrap a C library as a Logos module](https://docs.logos.co/core/build-modules/wrap-a-c-library-as-a-logos-core-module.md#step-2-write-the-c-library):\n\n     ```bash\n     cd ../logos-calc-module/lib\n     gcc -shared -fPIC -o libcalc.so libcalc.c     # Linux\n     # gcc -shared -fPIC -o libcalc.dylib libcalc.c  # macOS\n     cd ../../logos-calc-ui-cpp\n     ```\n\n1. Stage the files and lock `calc_module` to your local project:\n\n   ```bash\n   git add -A\n   nix flake update --override-input calc_module path:../logos-calc-module\n   git add flake.lock\n   ```\n\n   - `--override-input` resolves `../logos-calc-module` to an absolute path and records it in `flake.lock`, so later commands need no override.\n   - The lock keeps a copy of `calc_module` as it is now. After you change `calc_module`, run the `nix flake update` command again to build against the change.\n\n1. Build and run the app:\n\n   ```bash\n   nix run\n   ```\n\n   - The first run builds the backend, the replica factory and `logos-standalone-app`, which can take several minutes.\n\n   **Expected result:** `logos-standalone-app` opens the calculator view, which shows **Connected** once the backend is ready:\n\n   ![Logos C++ calculator view with its operation buttons](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/core/assets/build-a-logos-cpp-ui-module/calc-cpp-buttons.png)\n\n1. Enter `3` and `5` in the first two fields, then click **Add**.\n\n   **Expected result:** the result shows `8`, followed by `Computations: 1` and `Last op (signal): add = 8`:\n\n   ![Logos C++ calculator view showing the result of 3 + 5](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/core/assets/build-a-logos-cpp-ui-module/calc-cpp-result.png)\n\n   - `8` is the slot's reply, `Computations: 1` is the `computeCount` property and `Last op (signal): add = 8` is the `computed` signal.\n\n1. Click **Store (MS)**, then **Announce version (event)**.\n\n   **Expected result:** the view shows `Memory: 8` and `Version event: 1.0.0`.\n\n   - `Memory` is the `READWRITE` property: the view wrote it, and the value synced back from the backend.\n   - `Version event` arrives through the event path: the button made `calc_module` emit `versionReady`, and the backend's subscription wrote the payload to the `versionEvent` property.\n\n## Step 10: Reload the view as you edit it (optional)\n\n`nix run` rebuilds the plugin whenever any tracked file changes, including the QML. The development launcher reloads the view from your source tree instead, without a rebuild.\n\n1. Build the development launcher once, then start it from the project root:\n\n   ```bash\n   nix build .#ui-dev\n   ./result/bin/run-logos-standalone-ui\n   ```\n\n   It reports where it reloads QML from:\n\n   ```text\n   run-logos-standalone-ui: hot-reloading QML from /path/to/logos-calc-ui-cpp/src/qml\n     (export DEV_QML_PATH to override, or LOGOS_QML_HOT_RELOAD=0 to disable)\n   ```\n\n1. Edit and save a `.qml` or `.js` file under `src/qml/`.\n\n   **Expected result:** the view re-renders with your change, and the launcher logs `QML reloaded` with the file's path.\n\n   - The backend keeps running in its own process, so its state and connections survive a reload. QML state, such as the text in a field, resets.\n   - A QML syntax error is logged with its line number and blanks the view until the next save that compiles.\n   - Changes to C++, the `.rep` file, `metadata.json` or CMake still need a rebuild: run `nix build .#ui-dev` again and restart the launcher.\n\n## Step 11: Add UI integration tests (optional)\n\nAdd automated UI tests with the [logos-qt-mcp](https://github.com/logos-co/logos-qt-mcp) test framework. The tests connect to the QML inspector in `logos-standalone-app` and can find elements, click buttons and check text.\n\n1. Create `tests/ui-tests.mjs`:\n\n   ```javascript\n   import { resolve } from \"node:path\";\n\n   // CI sets LOGOS_QT_MCP automatically; for interactive use: nix build .#test-framework -o result-mcp\n   const root =\n     process.env.LOGOS_QT_MCP ||\n     new URL(\"../result-mcp\", import.meta.url).pathname;\n   const { test, run } = await import(\n     resolve(root, \"test-framework/framework.mjs\")\n   );\n\n   test(\"calc_ui_cpp: loads and shows title\", async (app) => {\n     await app.waitFor(\n       async () => {\n         await app.expectTexts([\"Logos Calculator (C++ backend)\"]);\n       },\n       { timeout: 15000, interval: 500, description: \"UI to load\" },\n     );\n   });\n\n   test(\"calc_ui_cpp: operation buttons visible\", async (app) => {\n     await app.expectTexts([\"Add\", \"Multiply\", \"Factorial\", \"Fibonacci\"]);\n   });\n\n   run();\n   ```\n\n1. Stage the test file and run the tests headlessly:\n\n   ```bash\n   git add tests/\n   nix build .#integration-test -L\n   ```\n\n   - `integration-test` launches `logos-standalone-app` with `QT_QPA_PLATFORM=offscreen`, so no display is needed, and runs every `.mjs` file in `tests/`.\n\n1. To run the tests against a running app instead, build the test framework and start the app:\n\n   ```bash\n   nix build .#test-framework -o result-mcp\n   nix run .\n   ```\n\n   Then, in a second terminal in the project directory, run the tests with [Node.js](https://nodejs.org/):\n\n   ```bash\n   node tests/ui-tests.mjs\n   ```\n\n   - The app serves its QML inspector on port 3768.\n\n## Troubleshooting the C++ UI module build\n\n### Linker errors during `nix build`\n\nConfirm that `../logos-calc-module/lib/libcalc.so` (Linux) or `../logos-calc-module/lib/libcalc.dylib` (macOS) exists, and that `flake.lock` was updated with `nix flake update --override-input calc_module path:../logos-calc-module`. A missing library or a lock that still holds the placeholder path is the most common cause.\n\n### The development launcher doesn't find your QML\n\nThe launcher looks for the `view` entry from `metadata.json` under `src/<viewDir>/`, then under `<viewDir>/`. If your project is laid out differently, set `DEV_QML_PATH` to the directory that contains the entry file itself, here `Main.qml`, not a parent directory.\n\n### Known constraints\n\nReloading works only with `logos-standalone-app`. Logos Basecamp loads UI plugins from its own data directory, so it shows your changes only after you rebuild and reinstall the LGX package.\n"},{"id":"core/reference/lgx-package-format-and-bundling-reference","title":"LGX package format and bundling reference","summary":"An LGX file (.lgx) is the package format for distributing a Logos module: a deterministic, gzip-compressed tar archive that bundles one or more platform-specific builds of the module with a signed manifest describing its metadata and dependencies. lgx is the CLI that creates, inspects, and signs these archives. logosctl installs them into a session, from a catalogue or from a local file; the standalone lgpm installs local files, and lgpd downloads packages from a catalogue without installing them.","section":"core","doc_type":"reference","url":"https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference","markdown_url":"https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md","permalink":"/core/reference/lgx-package-format-and-bundling-reference","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/reference/lgx-package-format-and-bundling-reference.md","headings":[{"depth":2,"text":"Package structure"},{"depth":2,"text":"Variants and bundling"},{"depth":2,"text":"Manifest fields"},{"depth":3,"text":"The `ui_qml` contract"},{"depth":2,"text":"Signing and verification"},{"depth":2,"text":"Further reading"}],"markdown":"# LGX package format and bundling reference\n\nAn LGX file (`.lgx`) is the [package](https://docs.logos.co/get-started/glossary.md#package) format for distributing a Logos [module](https://docs.logos.co/get-started/glossary.md#module): a deterministic, gzip-compressed tar archive that bundles one or more platform-specific builds of the module with a signed manifest describing its metadata and dependencies. `lgx` is the CLI that creates, inspects, and signs these archives. [`logosctl`](https://docs.logos.co/core/reference/logos-cli-reference.md) installs them into a session, from a [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue) or from a local file; the standalone `lgpm` installs local files, and `lgpd` downloads packages from a catalogue without installing them.\n\nThis page summarises the format. The [`logos-package` spec](https://github.com/logos-co/logos-package/blob/master/docs/spec.md) is the authoritative reference for the manifest schema and signing details, and the `logos-package` [command reference](https://github.com/logos-co/logos-package#command-reference) lists every `lgx` command; the [catalogue format spec](https://github.com/logos-co/logos-modules-release-tool/blob/main/docs/catalog-format.md) covers how packages are listed and verified for download.\n\n## Package structure\n\n```\npackage.lgx (tar.gz)\n├── manifest.json          # Required - package metadata\n├── manifest.sig           # Optional - Ed25519 signature with DID identity\n├── assets/                # Optional - variant-independent package assets\n│   ├── icon.png           #   Package icon: PNG, exactly 256x256\n│   └── lidl/              #   Canonical module interface documents\n│       └── <name>.lidl\n├── variants/              # Required - one directory per platform build\n│   ├── linux-amd64/\n│   ├── darwin-arm64/\n│   └── windows-x86_64/\n├── docs/                  # Optional - documentation\n└── licenses/              # Optional - license files\n```\n\nOnly these entries are permitted at the archive root. Each entry under `variants/` is a platform build, named as described in [Variants and bundling](#variants-and-bundling); files directly under `variants/` are not allowed. Everything under `assets/` is platform-independent and stored once, so a registry can read a package's icon or `.lidl` interface documents without unpacking a platform build.\n\nBuilding a package with `lgx` is deterministic: identical inputs always produce a byte-identical `.lgx`. A catalogue publishes each package's content hash (`rootHash`, the root of the Merkle tree over its contents), and installers check a download against it before installing, rather than trusting the transport.\n\n## Variants and bundling\n\nA variant is named after the platform it runs on. A development variant adds `-dev`: its libraries still resolve from the Nix store, so only an installer built with Nix accepts it. A portable variant bundles its libraries and runs anywhere.\n\n| Platform | Portable variant | Development variant |\n|---|---|---|\n| Linux x86_64 | `linux-amd64` | `linux-amd64-dev` |\n| Linux aarch64 | `linux-arm64` | `linux-arm64-dev` |\n| macOS arm64 | `darwin-arm64` | `darwin-arm64-dev` |\n| macOS x86_64 | `darwin-amd64` | `darwin-amd64-dev` |\n| Windows x86_64 | `windows-x86_64` | `windows-x86_64-dev` |\n\nAn installer accepts only variants for its own platform, and only of its own kind: a released `logosctl`, `lgpm` or Basecamp takes portable variants, and a development build of one takes `-dev` variants. Installers from `logos-package-manager` 0.3.0 onwards also accept the other spelling of an architecture (`x86_64` or `amd64`, `aarch64` or `arm64`), but older ones do not, so build packages with the names in the table.\n\nA module built with `logos-module-builder` has these package outputs:\n\n| Output | Produces |\n|---|---|\n| `nix build .#lgx` | The development variant for the build machine |\n| `nix build .#lgx-portable` | The portable variant for the build machine |\n| `nix build '.#packages.x86_64-windows.lgx-portable'` | The `windows-x86_64` variant, cross-built on Linux, since Nix does not run on Windows |\n\nThe outputs are made with [`nix-bundle-lgx`](https://github.com/logos-co/nix-bundle-lgx), which can package the `lib/` output of any Nix build: `#default` makes the development variant, `#portable` the portable one, and `#dual` a package carrying both. To ship one package for several platforms, build a variant on each and combine them with `lgx merge`.\n\n`lgx` has no release binaries; build it with `nix build github:logos-co/logos-package#lgx`.\n\n## Manifest fields\n\n`manifest.json` is a UTF-8 JSON file. The fields most relevant to authoring and consuming a package:\n\n| Field | Type | Purpose |\n|---|---|---|\n| `manifestVersion` | string | Manifest schema version, currently `0.6.0`. Gates the version-dependent rules on this page: the icon contract (`0.4.0`+), `provides` (`0.5.0`+), and `optional_dependencies` and `interface_dependencies` (`0.6.0`+) |\n| `name`, `version` | string | Canonical lowercase package identity |\n| `description`, `author`, `category` | string | Required human-readable metadata |\n| `type` | string | Package classification, for example `core`, `library`, or `ui_qml` |\n| `dependencies` | array | Other packages this one requires to run |\n| `optional_dependencies` | array | Packages this one can call but does not require |\n| `interface_dependencies` | array | Names of interfaces the module binds to a provider at run time. Not resolved to packages; carried so a catalogue or UI can show them |\n| `main` | object | Map of platform variant name to the entry point path for that variant |\n| `display_name` | string | Human-readable label shown by UI consumers (Package Manager, Apps Inspector) and CLI tools. Falls back to `name` when absent |\n| `provides` | array | App-to-app intents the package can service, for example `chat.group.open` |\n| `icon` | string | Path to the package's `assets/icon.png`. Required for `type: \"ui_qml\"` at `manifestVersion` `0.4.0`+; optional for every other type |\n\nSee the [full field reference](https://github.com/logos-co/logos-package/blob/master/docs/spec.md#manifest-schema) for the complete schema, including dependency version ranges and signer pins.\n\n### The `ui_qml` contract\n\nA package's `type` distinguishes a [core module](https://docs.logos.co/get-started/glossary.md#core-module) from a [UI module](https://docs.logos.co/get-started/glossary.md#ui-module):\n\n- A **core module** package uses `main` as its per-variant entry point, same as most other package types.\n- A **UI module** package sets `type: \"ui_qml\"` and adds a `view` field: the relative path to its QML entry point, identical across variants. `main` becomes optional for this type—when present, it points at a per-variant backend Qt plugin that the host runs in an isolated process and bridges to the QML view; when absent, the QML view loads directly in-process and the package's variant directories carry only its QML files. `variants/` is required either way: it is the directory that is mandatory, not its contents, so a package with no native binary is still valid. From `manifestVersion` `0.4.0` onwards, `type: \"ui_qml\"` packages must also ship a 256x256 `assets/icon.png`, since these are the packages rendered as tiles in Basecamp's app grid and sidebar.\n\n## Signing and verification\n\nPackages can be signed with an Ed25519 key identified by a `did:jwk:...` DID, producing a `manifest.sig` alongside `manifest.json`. Content hashes (a Merkle tree over the archive) are always present in the manifest regardless of signing, and `lgx verify` recomputes and checks them against the archive contents. A package without them, made by older tooling, fails validation, so installers reject it; rebuild it with current tooling.\n\nInstallers apply a signature policy at install time. By default an unsigned package, or one signed by a key the installer does not trust, installs with a warning:\n\n- `logosctl` reads the policy from the `signature_policy` setting of its daemon configuration (`none`, `warn` or `require`) and trusts the keys added with `logosctl key add`, which are kept per session.\n- `lgpm install` rejects such a package with `--require-signatures`, and trusts the keys added with `lgx keyring add`.\n- Basecamp's package manager keeps its own keyring.\n\nTrust comes only from the keyring of the installer itself. A signer named in a package, in a catalogue entry, or in a catalogue's `trustedSigners` list is not trusted until you add its key.\n\n## Further reading\n\n- [`logos-package` spec](https://github.com/logos-co/logos-package/blob/master/docs/spec.md)—manifest schema, `lgx` CLI reference, signing and DID details.\n- [Logos catalogue format spec](https://github.com/logos-co/logos-modules-release-tool/blob/main/docs/catalog-format.md)—for `logos-repo.json` and `index.json`, version selection, and download verification.\n- [Build and run a Logos core module](https://docs.logos.co/core/build-modules/build-and-run-a-logos-core-module.md)—building and installing an `.lgx` package end to end.\n- [Logos CLI reference](https://docs.logos.co/core/reference/logos-cli-reference.md)—installing packages and managing trusted keys with `logosctl`.\n- [`nix-bundle-lgx`](https://github.com/logos-co/nix-bundle-lgx)—the tool behind the builder's package outputs.\n"},{"id":"core/reference/logos-cli-reference","title":"Logos CLI Reference","summary":"logosctl is the command-line tool for running Logos modules without the Basecamp app. It runs the Logos Core runtime as a daemon, which you drive with client commands to install packages, load modules, call their methods and watch their events. It bundles the package manager and package downloader modules, so searching a catalogue, installing with dependencies resolved, loading and calling all go through one binary.","section":"core","doc_type":"reference","url":"https://docs.logos.co/core/reference/logos-cli-reference","markdown_url":"https://docs.logos.co/core/reference/logos-cli-reference.md","permalink":"/core/reference/logos-cli-reference","source":"https://github.com/logos-co/logos-docs/blob/main/docs/core/reference/logos-cli-reference.md","headings":[{"depth":2,"text":"Install"},{"depth":2,"text":"Sessions"},{"depth":2,"text":"Global options"},{"depth":2,"text":"Commands"},{"depth":3,"text":"Daemon"},{"depth":3,"text":"Modules"},{"depth":3,"text":"Calling and watching"},{"depth":3,"text":"Packages"},{"depth":3,"text":"Catalogues"},{"depth":3,"text":"Signing keys"},{"depth":2,"text":"Argument typing"},{"depth":2,"text":"Daemon configuration"},{"depth":2,"text":"Exit codes"},{"depth":2,"text":"Other Logos command-line tools"},{"depth":2,"text":"Further reading"}],"markdown":"# Logos CLI Reference\n\n`logosctl` is the command-line tool for running Logos [modules](https://docs.logos.co/get-started/glossary.md#module) without the Basecamp app. It runs the Logos Core runtime as a daemon, which you drive with client commands to install [packages](https://docs.logos.co/get-started/glossary.md#package), load modules, call their methods and watch their events. It bundles the package manager and package downloader modules, so searching a [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue), installing with dependencies resolved, loading and calling all go through one binary.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis reference describes [`logosctl` 0.3.2](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2). The complete manual, including every configuration key, is [`docs/logosctl.md`](https://github.com/logos-co/logos-logoscore-cli/blob/0.3.2/docs/logosctl.md) in `logos-logoscore-cli`.\n\n## Install\n\n| Platform | Command |\n|:---|:---|\n| Linux x86_64 or aarch64, macOS arm64 | `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh \\| sudo sh` |\n| Windows x86_64 (PowerShell) | `irm https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.ps1 \\| iex` |\n\nBoth scripts install the release assets published on the [release page](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2). There is no build for macOS on Intel. To build `logosctl` from source, run `nix build github:logos-co/logos-logoscore-cli#ctl`.\n\n## Sessions\n\nEverything `logosctl` needs lives in one directory, a session. `--config-dir` selects it, and so does the `LOGOSCTL_CONFIG_DIR` environment variable. The default is `~/.logosctl`. Each session runs at most one daemon, and two sessions can hold different packages and trust different signers.\n\n| Path | Contents |\n|:---|:---|\n| `daemon/config.yaml` | Daemon configuration, installed with `logosctl daemon config set` |\n| `modules/` | Core modules installed into this session |\n| `plugins/` | UI plugins installed into this session |\n| `keyring/` | Trusted package-signing keys |\n| `logs/` | Daemon log; `logs/daemon.log` always points at the current file |\n| `cache/downloads/` | Downloaded `.lgx` files |\n| `data/` | Per-module persistent data |\n\nBesides the session's own modules, `logosctl` ships with `capability_module` and `modules_state`, which are part of the runtime, and with `package_manager`, `package_downloader` and `storage_module` as embedded packages. A module installed into the session with the same name takes precedence over an embedded one.\n\n## Global options\n\nThese options work before or after any command.\n\n| Option | Description |\n|:---|:---|\n| `--config-dir DIR` | Session directory to act on (default `~/.logosctl`, also `LOGOSCTL_CONFIG_DIR`) |\n| `-j`, `--json` | Force JSON output |\n| `--no-json`, `--human` | Force human-readable output, even when the output is piped |\n| `-q`, `--quiet` | Suppress non-essential output |\n| `-v`, `--verbose` | Show debug logs |\n| `--version` | Print the version and exit |\n| `-h`, `--help` | Show help |\n| `-D` | Start the daemon, the same as `daemon start` |\n| `-d`, `--detach` | With `-D` or `daemon start`: fork into the background and return once the daemon accepts commands |\n\nOutput is human-readable in a terminal and JSON when piped, unless one of the format options says otherwise.\n\n## Commands\n\nCommands are grouped by what they act on. Within a group, `ls` lists and `show` describes one item; `list` and `info` are accepted as aliases.\n\n### Daemon\n\n| Command | Description |\n|:---|:---|\n| `logosctl daemon start [--detach]` | Start the runtime. `--detach` returns once the daemon accepts commands, unlike `&`, which returns before it is ready. |\n| `logosctl daemon stop` | Shut the daemon down and return once it has exited. Also `logosctl stop`. |\n| `logosctl daemon status` | Daemon health, uptime and a module summary. Also `logosctl status`. |\n| `logosctl daemon config set FILE` | Validate and install the daemon configuration. `FILE` can also be `@file` or `-` for standard input. |\n| `logosctl daemon config show` | Print the daemon configuration and where it lives |\n\n### Modules\n\n| Command | Description |\n|:---|:---|\n| `logosctl module ls [--loaded]` | List known modules, or only the loaded ones |\n| `logosctl module show NAME` | Methods and events with their descriptions, dependencies and crash details |\n| `logosctl module load NAME [--no-optional]` | Load a module and its dependencies. Installed optional dependencies load too, unless `--no-optional` is passed. |\n| `logosctl module unload NAME [--no-dependents]` | Unload a module and, unless `--no-dependents` is passed, the modules that depend on it |\n| `logosctl module reload NAME` | Reload a module |\n| `logosctl module stats` | CPU and memory use per module. Also `logosctl stats`. |\n\n### Calling and watching\n\n| Command | Description |\n|:---|:---|\n| `logosctl call MODULE METHOD [ARGS...]` | Call a method on a loaded module. See [argument typing](#argument-typing). |\n| `logosctl watch MODULE [--event NAME]` | Stream the events a module emits, or only the named event |\n\n### Packages\n\n| Command | Description |\n|:---|:---|\n| `logosctl package install NAME\\|FILE.lgx ...` | Install from a catalogue, or from a local `.lgx` file. Also `logosctl install`. |\n| `logosctl package install --dir DIR` | Install every `.lgx` file in a directory |\n| `logosctl package upgrade NAME\\|FILE.lgx` | Upgrade an installed package |\n| `logosctl package remove NAME` | Remove a package and the packages that depend on it. Also `logosctl package uninstall`. |\n| `logosctl package ls [--type core\\|ui]` | List the packages installed in this session, including embedded ones |\n| `logosctl package show NAME\\|FILE.lgx` | Details and every available version of a package, or inspect an `.lgx` file without installing it |\n| `logosctl package deps NAME [-r\\|--recursive] [--reverse]` | List a package's dependencies, or with `--reverse` the packages that depend on it |\n| `logosctl package search [QUERY] [--category C] [--catalog C]` | Search the configured catalogues. Also `logosctl search`. |\n| `logosctl package download NAME [--version V] [-o DIR]` | Download an `.lgx` file without installing it |\n\n`install` and `upgrade` take these options:\n\n| Option | Description |\n|:---|:---|\n| `--file FILE.lgx` | Install a local file. An argument ending in `.lgx` is read as a file too. |\n| `--dir DIR` | Install every `.lgx` file in a directory |\n| `--version V`, `--root-hash H` | Select an exact release of a catalogue package |\n| `--catalog C` | Use only this catalogue |\n| `-y`, `--yes` | Apply without asking. Required when no terminal is available to confirm in. |\n| `--dry-run` | Print the changes, and which running modules they would stop, without applying them |\n| `--no-deps` | Do not install dependencies |\n| `--no-optional` | Install only required dependencies, not optional ones |\n| `--no-dependents` | Act on the named packages only, leaving the packages that depend on them in place |\n\n`remove` takes `-y`, `--dry-run` and `--no-dependents`.\n\n- Installing does not load a module. Load it with `logosctl module load NAME`.\n- The daemon rescans after an install, so no restart is needed. Only modules that were already running and had to be stopped are restarted.\n- Catalogue names and local files cannot be mixed in one command.\n- All package commands need a running daemon, which hosts the package manager.\n\n### Catalogues\n\n| Command | Description |\n|:---|:---|\n| `logosctl catalog ls` | List the configured catalogues. The default is `logos-modules-official`. |\n| `logosctl catalog add URL` | Add a catalogue by the URL of its `logos-repo.json`. `remove`, `enable` and `disable` take a URL too. |\n| `logosctl catalog refresh` | Fetch every enabled catalogue again |\n| `logosctl catalog source [any\\|logos\\|http]` | Show or set where packages download from: Logos Storage first then HTTP (`any`, the default), Logos Storage only (`logos`), or HTTP only (`http`) |\n\n### Signing keys\n\n| Command | Description |\n|:---|:---|\n| `logosctl key ls` | List the keys this session trusts |\n| `logosctl key add NAME --did DID [--display-name N] [--url U]` | Trust a signing key |\n| `logosctl key remove NAME` | Stop trusting a key |\n\nThe session's keyring is separate from Basecamp's. The `signature_policy` setting in the daemon configuration decides what happens to a package that is unsigned or signed by a key the session does not trust: `none` skips the check, `warn` (the default) installs it with a warning, and `require` refuses it.\n\n## Argument typing\n\n`logosctl call` turns each argument into a JSON value with the first rule that matches.\n\n| Argument | Becomes | Example |\n|:---|:---|:---|\n| `json:VALUE` | `VALUE` parsed as JSON | `json:[1,2,3]`, `json:{\"k\":\"v\"}` |\n| `json:@FILE` | The file's contents parsed as JSON | `json:@payload.json` |\n| `str:TEXT` | `TEXT` as a string, unchanged | `str:42` |\n| `@FILE` | The file's contents as a string | `@config.json` |\n| `true` or `false` | A Boolean | `true` |\n| A decimal whole number | An integer | `42`, `-7` |\n| A decimal number | A double | `3.14`, `1e6` |\n| Anything else | A string | `hello`, `0xf39F...` |\n\nBinary (`bstr`) arguments are JSON objects holding base64url content without padding: `json:{\"_bytes\":\"aGVsbG8\"}`.\n\n## Daemon configuration\n\nThe daemon configuration is a YAML document that you install with `logosctl daemon config set`. It replaces the previous configuration, takes effect at the next daemon start, and is validated first: an unknown key or a value of the wrong type is reported by name and nothing is written.\n\n| Key | Description |\n|:---|:---|\n| `dirs` | Redirect a session directory: `keyring`, `cache`, `modules`, `plugins`, `data` or `logs`. A relative path stays inside the session. |\n| `modules_dirs` | Extra read-only directories to scan for modules |\n| `signature_policy` | `none`, `warn` (default) or `require`. See [Signing keys](#signing-keys). |\n| `access_group` | Share the daemon with an operating-system group |\n| `access_policy` | Which caller modules may call which modules, as a JSON document in a string |\n| `logging` | Log file name, rotation size and number of files kept |\n\nSee [`docs/logosctl.md`](https://github.com/logos-co/logos-logoscore-cli/blob/0.3.2/docs/logosctl.md) for every key.\n\n## Exit codes\n\n| Code | Meaning |\n|:---|:---|\n| `0` | Success |\n| `1` | General error, or the daemon is not running (for `status`) |\n| `2` | No daemon running, or the daemon did not answer |\n| `3` | Module error: not found, or loading or unloading failed |\n| `4` | Method error: not found, the call failed, or it timed out |\n\n## Other Logos command-line tools\n\n`logosctl` is the tool that Logos releases ship. Module developers also use these standalone tools. Build them with Nix:\n\n| Tool | Purpose | Build |\n|:---|:---|:---|\n| `lgx` | Create, inspect, merge and sign LGX packages. See the [LGX reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md). | `nix build 'github:logos-co/logos-package/0.1.0#lgx'` |\n| `lgpm` | Install local `.lgx` files into a directory of your choice, such as a Logos Basecamp data directory | `nix build 'github:logos-co/logos-package-manager/0.3.0#cli'` |\n| `lm` | Show a built module's metadata, methods and events | `nix build 'github:logos-co/logos-module/0.3.0#lm'` |\n\n## Further reading\n\n- [Start a Logos module from the CLI](https://docs.logos.co/core/build-modules/start-a-logos-module-from-the-cli.md): build a module, install it with `logosctl` and call it.\n- [`logosctl` manual](https://github.com/logos-co/logos-logoscore-cli/blob/0.3.2/docs/logosctl.md): every command, configuration key and example.\n- [LGX package format and bundling reference](https://docs.logos.co/core/reference/lgx-package-format-and-bundling-reference.md)\n"},{"id":"messaging/introduction-to-logos-messaging","title":"Introduction to Logos Messaging","summary":"Learn how Logos can be used to build private, censorship-resistant communication applications.","section":"messaging","doc_type":null,"url":"https://docs.logos.co/messaging","markdown_url":"https://docs.logos.co/messaging.md","permalink":"/messaging","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/introduction-to-logos-messaging.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Architecture"},{"depth":3,"text":"Delivery"},{"depth":3,"text":"Chat"},{"depth":3,"text":"Security features"},{"depth":3,"text":"Logos Messaging and libp2p"}],"markdown":"# Introduction to Logos Messaging\n\n#### Learn how Logos can be used to build private, censorship-resistant communication applications.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Logos Messaging](https://docs.logos.co/get-started/glossary.md#logos-messaging) is the messaging layer of the Logos technology stack. It lets applications send messages, relay data, and coordinate with one another without relying on centralised servers or exposing metadata. It uses a peer-to-peer relay network where messages propagate across nodes with no central broker, supporting private one-to-one messaging, group messaging, and public broadcast channels.\n\n## The basics\n\n- Logos Messaging comprises two [modules](https://docs.logos.co/get-started/glossary.md#module): [Delivery](https://github.com/logos-messaging/logos-delivery), a peer-to-peer [transport](https://docs.logos.co/get-started/glossary.md#transport) layer with spam protection, and [Chat](https://github.com/logos-messaging/logos-chat), a library for private, end-to-end encrypted conversations built on top of Delivery.\n- Messages propagate across a peer-to-peer relay network with no central broker. Anyone can run a node and there is no gas fee for sending or receiving messages.\n- Delivery is modular. Applications combine a set of protocols—[Relay](https://docs.logos.co/messaging/concepts/protocols.md#relay), [RLN Relay](https://docs.logos.co/messaging/concepts/protocols.md#rln-relay), [Filter](https://docs.logos.co/messaging/concepts/protocols.md#filter), [Store](https://docs.logos.co/messaging/concepts/protocols.md#store), and [Light Push](https://docs.logos.co/messaging/concepts/protocols.md#light-push)—to balance anonymity, scalability, and latency for their own use case.\n- Logos Messaging is built on top of [libp2p](https://docs.logos.co/messaging/concepts/logos-delivery-and-libp2p.md), but adds a service network that anyone can use without deploying their own infrastructure, along with economic spam protection that libp2p does not provide.\n\n## Architecture\n\nLogos Messaging is implemented as two modules that sit on top of the Logos networking layer: [Delivery](https://github.com/logos-messaging/logos-delivery), which handles peer-to-peer transport, and [Chat](https://github.com/logos-messaging/logos-chat), which builds private conversations on top of it.\n\n### Delivery\n\nDelivery is the peer-to-peer messaging network at the base of the messaging stack, and the reference implementation of the Logos Messaging protocol. It provides publish-subscribe messaging over an open, censorship-resistant network with no central broker, and it protects the network from spam using [RLN](https://docs.logos.co/messaging/get-started/faq.md#what-are-rate-limiting-nullifiers-rln), which verifies a zero-knowledge proof rather than a publisher's identity.\n\nDelivery's functionality is split across a set of [protocols](https://docs.logos.co/messaging/concepts/protocols.md):\n\n- **Relay** extends [libp2p GossipSub](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) to provide the base publish/subscribe messaging that peers use to send and receive messages.\n- **RLN Relay** extends Relay with rate limiting nullifiers for economic, privacy-preserving spam prevention.\n- **Filter** lets [light nodes](https://docs.logos.co/get-started/glossary.md#light-node) selectively subscribe to messages by content topic, trading some privacy for lower bandwidth use.\n- **Store** temporarily retains messages so offline peers can retrieve what they missed, though it does not guarantee availability—applications that need durable storage should pair Delivery with [Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) instead.\n- **Light Push** lets bandwidth-constrained nodes publish a message and receive an acknowledgement that at least one peer received it, without guaranteeing network-wide propagation.\n\nThese protocols interact across three [network domains](https://docs.logos.co/messaging/concepts/network-domains.md): the discovery domain, where nodes locate other peers; the gossip domain, where Relay and RLN Relay distribute messages; and the request/response domain, where Store, Filter, and Light Push serve resource-limited nodes directly.\n\nMessages are routed using [content topics](https://docs.logos.co/messaging/concepts/content-topics.md), developer-set metadata strings that let protocols filter, route, and retrieve messages.. How narrowly or broadly an application scopes its content topics is a direct privacy trade-off: Filter, Store, and Light Push all disclose content topics to peers, so grouping many users under shared topics increases k-anonymity, while splitting traffic into topic \"buckets\" helps distribute load for high-traffic applications.\n\nDelivery is [transport-agnostic](https://docs.logos.co/messaging/concepts/transports.md), so developers choose the protocol that fits their environment: TCP by default for service nodes, and secure WebSocket for browsers, with WebRTC, WebTransport, and QUIC under research for future support.\n\nTo join the network, a node [bootstraps](https://docs.logos.co/get-started/glossary.md#bootstrapping) using one of several [peer discovery](https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-discovery.md) mechanisms—including static peers, [DNS discovery](https://docs.logos.co/get-started/glossary.md#dns-discovery), Discv5, and [Peer Exchange](https://docs.logos.co/get-started/glossary.md#peer-exchange)—and then keeps looking for peers to maintain a healthy Relay mesh, reserve backup connections, and find peers with specific capabilities such as Store or Filter.\n\n### Chat\n\nChat is a protocol and library that provides one-to-one and group conversations with end-to-end encryption, using de-MLS for group messaging. It uses Delivery as its transport, so applications get everything they need to add private chat without designing and securing their own messaging protocol from scratch.\n\n### Security features\n\nLogos Messaging's [security guarantees](https://docs.logos.co/messaging/concepts/security-features.md) vary by protocol and are documented openly in the underlying RFCs. Relay provides pseudonymity and unlinkability between publishers, subscribers, and their activity, backed by [GossipSub](https://docs.logos.co/get-started/glossary.md#gossipsub)'s peer-scoring mechanism and ongoing RLN research for spam protection. Data confidentiality, integrity, and authenticity are available through the [Waku](https://docs.logos.co/get-started/glossary.md#waku) Message format and Noise protocols. Protocols that require a direct connection, such as Store and Filter, trade away some of that unlinkability, since a node can associate a peer's `PeerID` with the topics or filters it queries.\n\n### Logos Messaging and libp2p\n\nLogos Messaging is [built on top of libp2p](https://docs.logos.co/messaging/concepts/logos-delivery-and-libp2p.md) and shares much of its terminology, but it goes further in two ways. Developers can use it without deploying their own infrastructure, though anyone can choose to run a node to help decentralise the network. It is also a turnkey solution for privacy-preserving, censorship-resistant, platform-agnostic messaging, including out-of-the-box support for offline and resource-limited devices through Store, Filter, and Light Push, plus economic spam protection through RLN Relay, which libp2p does not provide on its own.\n"},{"id":"messaging/get-started/send-1-1-messages-logos-chat","title":"Send 1:1 messages with the Logos Chat app","summary":"Try out end-to-end encrypted private messaging over the Logos network.","section":"messaging","doc_type":"procedure","url":"https://docs.logos.co/messaging/get-started/send-1-1-messages-logos-chat","markdown_url":"https://docs.logos.co/messaging/get-started/send-1-1-messages-logos-chat.md","permalink":"/messaging/get-started/send-1-1-messages-logos-chat","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/get-started/send-1-1-messages-logos-chat.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Run the Logos Chat app"},{"depth":2,"text":"Step 2: Start a conversation"},{"depth":2,"text":"Step 3: Send and receive messages"},{"depth":2,"text":"Troubleshooting Logos Chat"},{"depth":3,"text":"Messages never arrive and the left panel shows \"Waiting for connection…\""},{"depth":3,"text":"A previously open conversation has disappeared"}],"markdown":"# Send 1:1 messages with the Logos Chat app\n\n#### Try out end-to-end encrypted private messaging over the Logos network.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.2.1**.\n\nThis procedure shows how to use the [Logos Chat](https://docs.logos.co/get-started/glossary.md#logos-chat) app to exchange encrypted 1:1 messages between two running instances. The [app](https://docs.logos.co/get-started/glossary.md#app) is a QML and C++ UI built on top of the [`logos-chat-module`](https://github.com/logos-co/logos-chat-module), which wraps the [Logos Chat SDK](https://github.com/logos-messaging/logos-chat). It demonstrates the basic private-messaging capabilities of the Logos Chat [Module](https://docs.logos.co/get-started/glossary.md#module): ephemeral identity, address-based contact discovery, and encrypted messaging with no central server. Use this procedure to verify the setup works or to explore the messaging flow for development purposes.\n\nIdentity, conversations, and message history exist only while the app is running. Restarting an instance gives it a new identity and clears all conversations.\n\n> **Info: Prerequisites**\n>\n>\n> - [Basecamp installed](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - Network access.\n\n## What to expect\n\n- You can run the Logos Chat app without building from source by installing it through Logos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp).\n- You can exchange encrypted messages between two instances in real time once one instance has opened a conversation with the other's address.\n- You can verify delivery by confirming each message appears on the receiving instance within a few seconds.\n\n## Step 1: Run the Logos Chat app\n\nYou need two running instances to complete this procedure. Each instance can be started with the instructions below.\n\n1.  In the left bar of Logos Basecamp, select **Package Manager**.\n\n    ![Logos Basecamp screenshot](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-1-1-messages-logos-chat/basecamp-package-manager.png)\n1.  Find **Chat** (type `ui_qml`) in the [package](https://docs.logos.co/get-started/glossary.md#package) list and click **INSTALL** in its row. The **Install Package?** dialogue lists the `chat_module` and `delivery_module` packages it depends on. Click **Install** to confirm.\n\n    ![Logos Basecamp package installation screenshot](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-1-1-messages-logos-chat/basecamp-install-packages.png)\n1. Wait until the **Action** column reads **INSTALLED** for both **Chat** and **Chat Module**.\n1. In the left bar, select the new chat icon to launch the Logos Chat app.\n\n## Step 2: Start a conversation\n\nThe app auto-initialises on launch. The bottom-left panel shows your identity, an **Online** indicator, and your hexadecimal **address** with a copy button next to it. To open a conversation, one instance needs the other's address. The steps below refer to the two instances as **A** and **B**.\n\n![Logos Chat App UI screenshot](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-1-1-messages-logos-chat/chat-app-screenshot.png)\n\n**On instance B:**\n\n1. Wait until the bottom-left panel shows **Online**.\n1. Click the copy button next to your address.\n1. Send the address to instance A through any [out-of-band](https://docs.logos.co/get-started/glossary.md#out-of-band) [channel](https://docs.logos.co/get-started/glossary.md#channel).\n\n**On instance A:**\n\n1. Click **New chat**, then select **Direct message** (\"One person, by address\").\n1. In the **New DM** dialogue, paste B's address under **Paste the other user's address**.\n1. Click **Create**. A new conversation appears in A's conversation list.\n\nInstance B does not need to accept anything. The conversation appears in B's list when A's first message arrives, which you send in the next step.\n\n## Step 3: Send and receive messages\n\n1. On instance A, select the conversation you created in [Step 2](#step-2-start-a-conversation).\n1.  Type a message in the message input field, then press `Enter` or click the send button.\n\n    Your messages appear right-aligned in the chat panel; the counterparty's messages appear left-aligned, each with a timestamp.\n1.  Observe instance B.\n\n    **Expected result:** the conversation appears in B's list with an unread badge, and the exact message text appears as an incoming (left-aligned) bubble in B's chat panel within a few seconds. Select the conversation on B and reply; the reply appears as an incoming bubble in A.\n\n## Troubleshooting Logos Chat\n\n### Messages never arrive and the left panel shows \"Waiting for connection…\"\n\nBoth instances need to reach a shared bootstrap peer to connect to the peer-to-peer network and discover each other. Ensure that both instances have a stable internet connection for [peer discovery](https://docs.logos.co/get-started/glossary.md#peer-discovery).\n\n### A previously open conversation has disappeared\n\nConversations are ephemeral and are not persisted between sessions. Open a new conversation with **New chat** > **Direct message** using the other instance's current address.\n"},{"id":"messaging/get-started/send-group-messages-logos-chat","title":"Send group messages with the Logos Chat app","summary":"Create a three-member end-to-end encrypted group conversation in the Logos Chat app, invite members by address, and send a message that reaches all of them.","section":"messaging","doc_type":"procedure","url":"https://docs.logos.co/messaging/get-started/send-group-messages-logos-chat","markdown_url":"https://docs.logos.co/messaging/get-started/send-group-messages-logos-chat.md","permalink":"/messaging/get-started/send-group-messages-logos-chat","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/get-started/send-group-messages-logos-chat.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Run three instances of the Logos Chat app"},{"depth":2,"text":"Step 2: Collect Bob's and Carol's addresses"},{"depth":2,"text":"Step 3: Create the group"},{"depth":2,"text":"Step 4: Invite Bob"},{"depth":2,"text":"Step 5: Invite Carol"},{"depth":2,"text":"Step 6: Send a message to the group"},{"depth":2,"text":"Troubleshooting Logos Chat groups"},{"depth":3,"text":"An invited member stays on the roster as \"Waiting to join\" for minutes"},{"depth":3,"text":"A send fails right after a membership change"},{"depth":3,"text":"The account card never reaches Online, or the conversations pane shows \"Waiting for connection…\""},{"depth":3,"text":"Two instances on one host interfere with each other"}],"markdown":"# Send group messages with the Logos Chat app\n\n#### Create a three-member end-to-end encrypted group conversation in the Logos Chat app, invite members by address, and send a message that reaches all of them.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis procedure shows how to use the [Logos Chat](https://docs.logos.co/get-started/glossary.md#logos-chat) app to form an end-to-end encrypted group conversation between three running instances, here called **Alice**, **Bob**, and **Carol**. Group conversations use de-MLS encryption from the [`logos-chat-module`](https://github.com/logos-co/logos-chat-module), and [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) carries the encrypted messages between instances. Use this procedure to try group creation, invite-by-address, asynchronous membership changes, and messages that reach every member with their sender named.\n\nEach launch of the app starts a new identity with a new address. Conversations from an earlier launch stay readable under **Previous sessions** but can't be sent to, and a group comes back only once a member invites your new address. A group's name and description are fixed when it is created, and in this release membership only grows: members can't be removed and can't leave.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux\n>    - macOS\n> - Network access, so that the three instances can reach the `logos.test` network and the key-package registry\n> - [Basecamp installed](https://docs.logos.co/basecamp/install-logos-basecamp.md), version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later.\n> - [RLN membership registered](https://docs.logos.co/lez/rln/register-rln-membership-from-basecamp.md).\n>\n\n## What to expect\n\n- You can send a message to a group conversation and see it arrive in every member's window with the sender's name and avatar.\n- You can create a named group and invite members by pasting their addresses.\n- You can watch an invited member move from **Waiting to join** to a full member as the group commits the change.\n\n## Step 1: Run three instances of the Logos Chat app\n\nOne Basecamp instance is one chat identity, so use this option when the three participants are on separate machines.\n\n1. In the left bar, select **Package Manager**.\n1. Find **Chat** in the [package](https://docs.logos.co/get-started/glossary.md#package) list and click **INSTALL** in its row. Click **Install** in the **Install Package?** dialogue to confirm, along with the `chat_module` and `delivery_module` packages it depends on.\n1. Wait until the **Action** column reads **INSTALLED** for both **Chat** and **Chat Module**.\n1. In the left bar, select the chat icon to launch the Logos Chat app.\n\n## Step 2: Collect Bob's and Carol's addresses\n\n**In Bob's and Carol's windows:**\n\n1. Wait until the account card shows **Online**.\n1. Click the copy button next to the address on the account card (**Copy my address**).\n1. Send the address to Alice through any [out-of-band](https://docs.logos.co/get-started/glossary.md#out-of-band) communication channel.\n\n## Step 3: Create the group\n\n**In Alice's window:**\n\n1. Click **New chat** at the top of the conversations card, then select **Group** (\"Named, with members\").\n1. Fill in **Group name**, for example `Book Club`, and optionally **Description (optional)**, for example `Weekly sci-fi picks`. Neither can be changed after creation.\n1. Click **Create**.\n\n   **Expected result:** the group opens as the current conversation, with Alice as its only member. A right-hand column appears with the **Members** roster and **Add member** at its foot.\n\n   ![Logos Chat showing the new Book Club group with Alice as its only member and Add member at the foot of the Members roster](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-group-messages-logos-chat/group-created.png)\n\n## Step 4: Invite Bob\n\nMembership changes are asynchronous. The group's steward batches an add into an MLS commit after a commit-inactivity window of about 60 seconds, and the welcome goes out only after the commit lands, so a new member joins minutes after the invite. Wait for Bob to join before inviting Carol, so that the second add starts from a settled group.\n\n**In Alice's window:**\n\n1. In the **Members** card, click **Add member**.\n1. Paste Bob's address, then click **Add**.\n1. An explainer appears: \"Adding a member isn't instant. Membership updates apply on a timer, so a new member can take up to a minute to appear.\" Click **Add member** to confirm.\n\n   ![The Adding a member explainer with the Don't show this again option and the Add member button](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-group-messages-logos-chat/add-member-explainer.png)\n\n   - Select **Don't show this again** to skip the explainer next time.\n\n   **Expected result:** Bob appears on Alice's roster as a dimmed row reading **Waiting to join**. Some minutes later, the group appears in Bob's conversation list with the name Alice gave it, and his row on the roster stops being dimmed.\n\n## Step 5: Invite Carol\n\n1. In Alice's window, repeat [Step 4](#step-4-invite-bob) with Carol's address.\n\n   - Any member can add another; the invite routes from whoever proposed it.\n\n   ![Alice's Members roster with Bob joined and Carol as a dimmed row reading Waiting to join](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-group-messages-logos-chat/member-waiting-to-join.png)\n\n1. Wait until the group appears in Carol's conversation list.\n\n   **Expected result:** all three rosters converge on three members, none of them dimmed or reading **Waiting to join**. The conversation's **Details** panel, opened with the toggle in the thread header, shows **3 joined**.\n\n   ![The Details panel of the Book Club group showing 3 joined above a roster of three members](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-group-messages-logos-chat/group-details-three-joined.png)\n\n## Step 6: Send a message to the group\n\n1. In Alice's window, select the group, type a message in the composer, and press `Enter` or click the send arrow.\n\n   **Expected result:** the message appears right-aligned in Alice's thread. Within a few seconds to about two minutes, it appears in Bob's and Carol's threads as an incoming, left-aligned message with Alice's avatar and name above it. Sender attribution is what distinguishes a group thread from a direct message.\n1. In Bob's window, reply to the group.\n\n   **Expected result:** Bob's reply reaches both Alice and Carol.\n\n   ![Carol's Book Club thread showing Alice's message and Bob's reply as incoming messages, each with the sender's name and avatar](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/messaging/assets/send-group-messages-logos-chat/group-messages-received.png)\n\n## Troubleshooting Logos Chat groups\n\n### An invited member stays on the roster as \"Waiting to join\" for minutes\n\nThis is by design, not a hang. The add is a consensus proposal that the group's steward batches into an MLS commit after a commit-inactivity window of about 60 seconds, and the welcome only goes out after the commit lands. Wait, and invite one member at a time, letting each join land before the next invite.\n\n### A send fails right after a membership change\n\nde-MLS rejects sends during the brief window while the group finalises the epoch change. The failure shows on the status bar at the foot of the window. Retry after a few seconds.\n\n### The account card never reaches Online, or the conversations pane shows \"Waiting for connection…\"\n\nThe instance has no route to the `logos.test` fleet or to the key-package registry, for example because it is offline, sandboxed, or has its peer-to-peer ports blocked. Run it with real network access. The session logs, under **Logs** or **Full logs** in the app, say which module failed and where its log file is.\n\n### Two instances on one host interfere with each other\n\nThey were given the same session directory. Give each instance its own `--user-dir`.\n"},{"id":"messaging/get-started/faq-logos-messaging","title":"FAQ: Logos Messaging","summary":"Review common questions about how Logos Messaging works and how to build on it.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/get-started/faq","markdown_url":"https://docs.logos.co/messaging/get-started/faq.md","permalink":"/messaging/get-started/faq","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/get-started/faq-logos-messaging.md","headings":[{"depth":2,"text":"What are Logos Messaging and Logos Delivery?"},{"depth":2,"text":"Does messaging require a gas fee?"},{"depth":2,"text":"What encryption does Logos Delivery use?"},{"depth":2,"text":"Where does Logos Delivery store the messages?"},{"depth":2,"text":"Can Logos Delivery only be used for wallet-to-wallet messaging?"},{"depth":2,"text":"How does Logos Delivery differ from IPFS?"},{"depth":2,"text":"What are Rate Limiting Nullifiers (RLN)?"}],"markdown":"# FAQ: Logos Messaging\n\n#### Review common questions about how Logos Messaging works and how to build on it.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n## What are Logos Messaging and Logos Delivery?\n\n[Logos Messaging](https://docs.logos.co/get-started/glossary.md#logos-messaging) is the messaging component of the Logos technology stack, composed of [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) and [Logos Chat](https://docs.logos.co/get-started/glossary.md#logos-chat). Logos Delivery is a peer-to-peer messaging network with DoS protection via RLN (Rate Limiting Nullifiers). Any application can use it to send and receive messages over an open, censorship-resistant network.\n\nLogos Chat is a protocol and library that provides 1:1 and group conversations with end-to-end encryption using de-MLS - everything an application needs to add private chat without rolling its own protocol.\n\n## Does messaging require a gas fee?\n\nNo, sending and receiving messages using Logos Messaging involves no gas fee.\n\n## What encryption does Logos Delivery use?\n\nLogos Delivery uses libp2p noise encryption for node-to-node connections. However, no default encryption method is applied to the data sent over the network. This design choice enhances Logos Delivery's encryption flexibility, encouraging developers to use custom protocols or Logos Delivery message payload encryption methods freely.\n\n## Where does Logos Delivery store the messages?\n\nLogos Delivery's [Store protocol](https://docs.logos.co/messaging/concepts/protocols.md#store) is designed to temporarily store messages within the network. However, Logos Delivery does not guarantee the message's availability and recommends using [Logos Storage](https://logos.co/technology-stack/storage) for long-term storage.\n\n## Can Logos Delivery only be used for wallet-to-wallet messaging?\n\nNo, Logos Delivery is flexible and imposes no specific rules on identifiers.\n\n## How does Logos Delivery differ from IPFS?\n\nLogos Delivery focuses on short, ephemeral, real-time messages, while IPFS focuses on large, long-term data storage. Although there's an overlap between the two technologies, Logos Delivery does not currently support large data for privacy reasons.\n\n## What are Rate Limiting Nullifiers (RLN)?\n\n[Rate Limiting Nullifier](https://docs.logos.co/messaging/concepts/protocols.md#rln-relay) is a zero-knowledge (ZK) protocol enabling spam protection in a decentralised network while preserving privacy. Each message must be accompanied by a ZK proof, which [Relay](https://docs.logos.co/messaging/concepts/protocols.md#relay) nodes verify to ensure the publishers do not send more messages than they are allowed. The ZK proof does not leak any private information about message publishers - it only proves they are members of a set of users allowed to publish a certain number of messages per given time frame.\n"},{"id":"messaging/concepts/understand-logos-delivery-protocols","title":"Understand Logos Delivery protocols","summary":"Understand the modular protocols that let applications balance anonymity, scalability, and latency.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/protocols","markdown_url":"https://docs.logos.co/messaging/concepts/protocols.md","permalink":"/messaging/concepts/protocols","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-logos-delivery-protocols.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Relay"},{"depth":2,"text":"RLN Relay"},{"depth":2,"text":"Filter"},{"depth":2,"text":"Store"},{"depth":2,"text":"Light Push"},{"depth":2,"text":"Waku message"}],"markdown":"# Understand Logos Delivery protocols\n\n#### Understand the modular protocols that let applications balance anonymity, scalability, and latency.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) takes a modular approach, providing a range of protocols that enable applications to control the trade-offs involved in the [Anonymity Trilemma](https://eprint.iacr.org/2017/954.pdf). This flexibility empowers applications to make informed choices regarding the desired balance between anonymity, scalability, and latency.\n\n## The basics\n \n- Logos Delivery is modular, so applications combine protocols to balance anonymity, scalability, and latency.\n- [Relay](#relay) provides the base publish/subscribe messaging, and [RLN relay](#rln-relay) extends it with economic spam prevention.\n- [Filter](#filter), [Store](#store), and [Light push](#light-push) adapt access for nodes with limited bandwidth or intermittent connections.\n\n## Relay\n \n[`Relay` protocol](https://lip.logos.co/messaging/core/stable/11/relay.html) employs a Pub/Sub architecture to facilitate the sending and receiving of messages among peers. It extends the [libp2p GossipSub protocol](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md) to create a privacy-focused peer-to-peer messaging protocol that enables secure communication channels, encryption, and protection against censorship.\n\n## RLN Relay\n \n[`RLN Relay` protocol](https://lip.logos.co/messaging/core/draft/17/rln-relay.html) extends the `Relay` protocol by using [Rate Limit Nullifiers (RLN)](https://lip.logos.co/anoncomms/draft/32/rln-v1.html) to provide efficient and economic spam-prevention. It enforces a rate limit on messages over time for all peers in the network, economically preventing spam, and imposes financial penalties and network removal for spammers. You can find more details in the [RLN Relay blog post](https://research.logos.co/rlog/rln-relay).\n \n## Filter\n \n[`Filter` protocol](https://lip.logos.co/messaging/core/draft/12/filter.html) allows [light nodes](https://docs.logos.co/get-started/glossary.md#light-node) to selectively subscribe to specific messages relayed by other peers using [content topics](https://docs.logos.co/messaging/concepts/content-topics.md). It is designed to be a lightweight alternative for accessing the `Relay` network, particularly tailored for devices with limited bandwidth.\n\n> **Info**\n>\n> `Filter` protocol helps optimise bandwidth usage, but it has fewer privacy guarantees as it must disclose the content topic to its peers to retrieve messages.\n \n## Store\n \n[`Store` protocol](https://lip.logos.co/messaging/core/draft/13/store.html) is responsible for storing messages relayed in the network, making it possible to query and retrieve them later. This functionality benefits offline peers by enabling them to retrieve missed messages upon reconnection.\n\n> **Info**\n>\n> Using `Relay` and `Filter` protocols is recommended when a node is online, as `Store` does not guarantee data availability. The `Store` protocol is suitable for retrieving messages when connecting to the network, like when an app starts.\n \n## Light Push\n \n[`Light Push`](https://lip.logos.co/messaging/core/draft/19/lightpush.html) is a [Request/Response](https://docs.waku.org/learn/concepts/network-domains#requestresponse-domain) protocol for nodes with limited bandwidth and short connection windows. It allows a client to receive an acknowledgement when sending messages, indicating that at least one peer has received them. Subsequently, the remote peer forwards these messages to the `Relay` network.\n\n> **Info**\n>\n> While the `Light Push` protocol acknowledges the receipt by the remote peer, it does not guarantee network-wide propagation.\n \n## Waku message\n \n[`Waku message`](https://lip.logos.co/messaging/core/stable/14/message.html) specifies the structure and format of messages. It includes the following attributes:\n \n- `content_topic` attribute for [content-based filtering](https://docs.logos.co/messaging/concepts/content-topics.md).\n- `payload` attribute containing the message data payload to be sent.\n- `meta` attribute for conveying additional details to various protocols for application-specific processing.\n- `timestamp` attribute signifying the time at which the message was generated by its sender.\n- `ephemeral` attribute specifying whether the network should not store the message.\n"},{"id":"messaging/concepts/about-content-topics","title":"About content topics","summary":"Understand how content topics filter and route messages, and how naming choices affect privacy.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/content-topics","markdown_url":"https://docs.logos.co/messaging/concepts/content-topics.md","permalink":"/messaging/concepts/content-topics","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/about-content-topics.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Naming format"},{"depth":2,"text":"Naming considerations"},{"depth":3,"text":"Protocols disclose content topics to peers"},{"depth":3,"text":"Increasing k-anonymity preserves user anonymity"},{"depth":3,"text":"Creating buckets help in distributing traffic"}],"markdown":"# About content topics\n \n#### Understand how content topics filter and route messages, and how naming choices affect privacy.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n \n`Content Topics` are metadata strings set by developers on outgoing messages to support protocol-level features like selectively processing incoming messages ([Relay](https://docs.logos.co/messaging/concepts/protocols.md#relay) or [Filter](https://docs.logos.co/messaging/concepts/protocols.md#filter)) and retrieving historical messages ([Store](https://docs.logos.co/messaging/concepts/protocols.md#store)) that meet specific filtering criteria.\n\n> **Info**\n>\n> Check out the [WAKU2-TOPICS](https://lip.logos.co/messaging/informational/draft/23/topics.html#content-topics) specification to learn more.\n \n## The basics\n \n- [Content topics](https://docs.logos.co/get-started/glossary.md#content-topic) are developer-set metadata strings that let protocols filter, route, and retrieve messages.\n- The recommended [naming format](#naming-format) combines an application name, version, content topic name, and encoding.\n- [Naming considerations](#naming-considerations) matter for privacy, because some protocols disclose content topics to peers.\n\n## Naming format\n\nHere is the recommended format for content topics:\n\n`/{application-name}/{version}/{content-topic-name}/{encoding}`\n\n- `application-name`: This is the unique name of your decentralised application to prevent conflicts with other apps.\n- `version`: Typically starting at `1`, this field helps track breaking changes in your messages.\n- `content-topic-name`: The specific name of the content topic used for filtering.\n- `encoding`: The message encoding or serialisation format, with [Protocol Buffers](https://protobuf.dev/) (`proto`) being the recommended choice.\n\nFor example, if your app is called `SuperCrypto` and it allows users to receive notifications and send private messages, you can consider using the following content topics:\n\n- `/supercrypto/1/notification/proto`\n- `/supercrypto/1/private-message/proto`\n\n> **Tip**\n>\n> While you can choose any encoding format for your `Content Topic`, we highly recommend using Protocol Buffers (`proto`) because of its efficiency. Choosing a lightweight format ensures optimal performance of your app.\n\n## Naming considerations\n\nWhen choosing a content topic, it is essential to consider the privacy implications. Here are several factors to keep in mind:\n\n### Protocols disclose content topics to peers\n\nThe `Filter`, `Store`, and `Light Push` protocols share content topics with peers, allowing them to link IP and content topic interests. The `Relay` protocol, using `GossipSub`, provides recipient anonymity, which can be compromised if the content topic exposes user information.\n\nFor example, instead of using Personally Identifiable Information (PII) in the content topic (for example, a public key), you can create buckets (for example, based on the first 4 bytes of the public key hash).\n\n> **Info**\n>\n> [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) is developing privacy-preserving features like [Anonymous Filter Subscription](https://lip.logos.co/messaging/core/draft/12/previous-versions/00/filter.html#future-work) for the `Filter` protocol and [Anonymous Query](https://lip.logos.co/messaging/core/draft/13/store.html#future-work) for the `Store` protocol to hide content topics from potential adversaries.\n\n### Increasing k-anonymity preserves user anonymity\n\nYou can increase [k-anonymity](https://www.privitar.com/blog/k-anonymity-an-introduction/) within the network by using a unified content topic across the entire application or targeting specific features like notifications or private messages, allowing multiple users to share it.\n\nWe recommend switching functionality using the Protocol Buffer (`proto`) message format. By doing so, applications can retain a high granularity and functionality while using a single content topic, preserving user privacy. For example:\n\n```js\nmessage NotificationPayload {\n...\n}\n\nmessage FeatureAbcPayload {\n...\n}\n\n// By default, all fields in protobuf are optional so only field may be encoded at a time\nmessage Payload {\n\tNotificationPayload notification = 1;\n\tFeatureAbcPayload feature_abc = 2;\n}\n```\n\n### Creating buckets help in distributing traffic\n\nWhen an application uses a single content topic, all users using [request/response protocols](https://docs.logos.co/messaging/concepts/network-domains.md#requestresponse-domain) (`Filter`, `Store`) receive all its messages. For heavy traffic, developers can create buckets by hashing a unique identifier (for example, recipient's ID, public key, or app domain topic) and adding its first byte to the content topic, like `/my-app/0/a/proto`.\n\nThis approach divides traffic into multiple topics, reducing the messages users have to download. Developers can add more first bytes to the content topic over time to improve efficiency and privacy based on messages and user needs.\n\n> **Info**\n>\n> The **k** value of **k-anonymity** equals the number of IDs for which the first character of the hash is `\"a\"`. For example, using a single content topic in an application with 10,000 users results in **k = 10,000**. However, using the hash ID's first character, **k** reduces to **10,000 / 16 = 625**.\n"},{"id":"messaging/concepts/about-network-domains","title":"About network domains","summary":"Understand the discovery, gossip, and request/response domains that structure how peers interact.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/network-domains","markdown_url":"https://docs.logos.co/messaging/concepts/network-domains.md","permalink":"/messaging/concepts/network-domains","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/about-network-domains.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Discovery domain"},{"depth":2,"text":"Gossip domain"},{"depth":2,"text":"Request/response domain"},{"depth":2,"text":"Overview of protocol interaction"}],"markdown":"# About network domains\n \n#### Understand the discovery, gossip, and request/response domains that structure how peers interact.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) is a unified and cohesive entity that offers a rich ecosystem with three distinct network interaction domains. These domains serve specialised purposes and contribute to the robust functionality of Logos Delivery, forming its foundation.\n\n## The basics\n \n- The [discovery domain](#discovery-domain) helps nodes locate other peers in the network.\n- The [gossip domain](#gossip-domain) distributes messages across the network through Relay.\n- The [request/response domain](#requestresponse-domain) serves resource-limited nodes with Store, Filter, and Light Push.\n\n## Discovery domain\n \n[Peer discovery](https://docs.logos.co/get-started/glossary.md#peer-discovery) in Logos Delivery lets nodes locate other nodes within the network. As a modular protocol, Logos Delivery incorporates various discovery mechanisms, such as [Discv5](https://docs.logos.co/messaging/concepts/understand-peer-discovery/discv5.md) and [Peer Exchange](https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-exchange.md). These mechanisms allow developers to choose the most suitable options for their specific use cases and user environments, including mobile phones, desktop browsers, servers, and more.\n \n## Gossip domain\n \n[GossipSub](https://docs.logos.co/get-started/glossary.md#gossipsub) derives its name from the practice within Pub/Sub networks where peers gossip about the messages they have encountered, thus establishing a message delivery network.\n \nLogos Delivery employs gossiping through [Relay](https://docs.waku.org/learn/concepts/protocols#relay) to distribute messages across the network. Additionally, Logos Delivery introduces [RLN Relay](https://docs.waku.org/learn/concepts/protocols#rln-relay), an experimental mechanism that combines privacy preservation and economic spam protection.\n \n## Request/response domain\n \nLogos Delivery provides a set of protocols to optimise its performance in resource-limited environments like low bandwidth or [mostly offline](https://docs.logos.co/get-started/glossary.md#mostly-offline) scenarios for multiple purposes.\n \n- [Store](https://docs.waku.org/learn/concepts/protocols#store) enables the retrieval of historical messages.\n- [Filter](https://docs.waku.org/learn/concepts/protocols#filter) efficiently retrieves a subset of messages to conserve bandwidth.\n- [Light Push](https://docs.waku.org/learn/concepts/protocols#light-push) supports message publication for nodes with limited bandwidth and short connection windows.\n \n## Overview of protocol interaction\n \nHere is a diagram illustrating the interaction between different protocols.\n\n```mermaid\nsequenceDiagram\n\tparticipant A as A relay\n\tparticipant B as B relay(pubtopic1)\n\tparticipant C as C relay(pubtopic1)\n\tparticipant D as D relay(pubtopic1), store(pubtopic1), filter\n\tparticipant E as E relay, store\n\tparticipant F as F filter\n\n\tA ->> A: msg1=WakuMessage(contentTopic1, data) (1)\n\tF ->> D: FilterRequest(pubtopic1, contentTopic1) (2)\n\tD ->> D: Subscribe F to filter (2)\n\tA ->> B: Publish msg1 on pubtopic1 (3)\n\tB ->> D: relay msg1 on pubtopic1 (3)\n\tD ->> D: store: saves msg1 (4)\n\tD ->> C: relay msg1 on pubtopic1 (4)\n\tD ->> F: MessagePush(msg1) (5)\n\tE ->> E: E comes online (6)\n\tE ->> D: HistoryQuery(pubtopic1, contentTopic1) (6)\n\tD ->> E: HistoryResponse(msg1, ...) (6)\n```\n\nThe Pub/Sub topic `pubtopic1` serves as a means of routing messages (the network employs a default Pub/Sub topic) and indicates that it is subscribed to messages on that topic for a relay. Node D serves as a `Store` and is responsible for storing messages.\n \n1. Node A creates a WakuMessage `msg1` with [Content Topic](https://docs.waku.org/learn/concepts/content-topics) `contentTopic1`.\n1. Node F requests to get messages filtered by Pub/Sub topic `pubtopic1` and Content Topic `contentTopic1`. Node D subscribes F to this filter and will forward messages that match that filter in the future.\n1. Node A publishes `msg1` on `pubtopic1`. The message is sent from Node A to Node B and then forwarded to Node D.\n1. Node D, upon receiving `msg1`, stores the message for future retrieval by other nodes and forwards it to Node C.\n1. Node D also pushes `msg1` to Node F, informing it about the arrival of a new message.\n1. At a later time, Node E comes online and requests messages matching `pubtopic1` and `contentTopic1` from Node D. Node D responds with `msg1` and potentially other messages that match the query.\n"},{"id":"messaging/concepts/about-transports","title":"About transports","summary":"Understand how nodes move data between peers and which transports suit each environment.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/transports","markdown_url":"https://docs.logos.co/messaging/concepts/transports.md","permalink":"/messaging/concepts/transports","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/about-transports.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Recommended transports"}],"markdown":"# About transports\n\n#### Understand how nodes move data between peers and which transports suit each environment.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Transports](https://docs.logos.co/get-started/glossary.md#transport) help move data packets across a network by establishing connections between peers. They define the rules and protocols to ensure efficient network transmission, routing, and data delivery. [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) is a transport-agnostic framework that allows developers to choose and support multiple protocols according to their requirements.\n\n## The basics\n\n- Transports move data packets between peers and define how the network transmits, routes, and delivers data.\n- Logos Delivery is transport-agnostic, so developers choose the protocols that suit their requirements.\n- The [recommended transports](#recommended-transports) vary by environment, from TCP for service nodes to secure WebSocket for browsers.\n\n## Recommended transports\n\nFor Logos Delivery nodes, the following transports are recommended:\n\n- **TCP**: By default, Logos Delivery nodes use TCP for communication. Service nodes should employ TCP for listening to and connecting with other nodes.\n- **Secure WebSocket**: In browser environments, secure WebSocket is used. Service nodes are encouraged to set up SSL certificates to enable incoming connections from browsers and serve them securely.\n- Other protocols like [WebRTC](https://github.com/logos-messaging/logos-delivery-js/issues/20), [WebTransport](https://github.com/logos-messaging/logos-delivery-js/issues/697), and QUIC have been researched and studied for potential integration.\n\n> **Info**\n>\n> Logos Delivery ensures compatibility and improved communication capabilities by following these recommended transports.\n"},{"id":"messaging/concepts/understand-peer-discovery/about-static-peers","title":"About static peers","summary":"Understand how hard-coded bootstrap peers work and the trade-offs they carry.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/static-peers","markdown_url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/static-peers.md","permalink":"/messaging/concepts/understand-peer-discovery/static-peers","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-peer-discovery/about-static-peers.md","headings":[{"depth":2,"text":"How static peers work"},{"depth":2,"text":"Pros and cons"}],"markdown":"# About static peers\n\n#### Understand how hard-coded bootstrap peers work and the trade-offs they carry.\n\n[Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) applications have the flexibility to embed bootstrap node addresses directly into their codebase. Developers can either use static peers operated by [Status](https://status.im/) or [run a node](https://docs.logos.co/messaging/delivery/run-delivery-node-from-cli.md).\n\n## How static peers work\n\n```mermaid\nsequenceDiagram\nAlice->>Bob: Connect to Bob (static peer details)\nAlice->>Carol: Connect to Carol (static peer details)\n```\n\nAlice establishes connections with Bob and Carol using their node details, which are predefined (hard-coded) into Alice's node.\n\n## Pros and cons\n\nPros:\n\n- Low latency.\n- Low resource requirements.\n\nCons:\n\n- Vulnerable to censorship: Node IPs can be blocked or restricted.\n- Limited scalability: The number of nodes is fixed and cannot easily be expanded.\n- Maintenance challenges: Updating the node list requires modifying the code, which can be cumbersome and involves releasing and deploying.\n"},{"id":"messaging/concepts/understand-peer-discovery/about-dns-discovery","title":"About DNS discovery","summary":"Understand how a node retrieves peer connection details from an ENR tree published in DNS.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/dns-discovery","markdown_url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/dns-discovery.md","permalink":"/messaging/concepts/understand-peer-discovery/dns-discovery","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-peer-discovery/about-dns-discovery.md","headings":[{"depth":2,"text":"How DNS discovery works"},{"depth":2,"text":"Pros and cons"}],"markdown":"# About DNS discovery\n\n#### Understand how a node retrieves peer connection details from an ENR tree published in DNS.\n\nBuilt upon the foundation of [EIP-1459: Node Discovery via DNS](https://eips.ethereum.org/EIPS/eip-1459), [DNS Discovery](https://docs.logos.co/get-started/glossary.md#dns-discovery) allows the retrieval of an `ENR` tree from the `TXT` field of a domain name. This approach enables the storage of essential node connection details, including IP, port, and multiaddr. This [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping) method allows anyone to register and publish a domain name for the network, promoting increased decentralisation.\n\n## How DNS discovery works\n\n```mermaid\nsequenceDiagram\n    Node->>DNS Server: (1) Lookup TXT example.com\n    DNS Server-->>Node: (2) enrtree-root:v1 e=U3...3Y ...\n    Node->>DNS Server: (3) Lookup TXT U3...3Y.example.com\n    DNS Server-->>Node: (4) enrtree-branch:DU...VQ,J3..HU,IC...WE\n    Node->> DNS Server: (5) Lookup TXT DU...VQ.example.com\n    DNS Server-->>Node: (6) enr:-M-4QLdAB-Kyz...Wt1Mg8\n    Node ->> Node: (7) Decode ENR: Peer's connection details\n```\n\n1. DNS lookup query to retrieve TXT data stored on `example.com` domain.\n1. `enrtree-root` is returned, and the value of `e` is the `enr-root`, the root hash of the node subtree.\n1. DNS lookup query to retrieve TXT data stored on `<enr-root>.example.com` domain.\n1. `enrtree-branch` is returned; this tree contains hashes of node subtrees.\n1. DNS lookup query to retrieve TXT data stored on `DU...VQ.example.com` domain, the first leaf of `enrtree-branch`.\n1. `enr` record is returned.\n1. Returned value is decoded, and peer connection details such as IP address and port are learned.\n\n## Pros and cons\n\nPros:\n\n- Low latency, low resource requirements.\n- Easy bootstrap list updates by modifying the domain name, eliminating the need for code changes.\n- Ability to reference a larger list of nodes by including other domain names in the code or [ENR](https://docs.logos.co/get-started/glossary.md#enr) tree.\n\nCons:\n\n- Vulnerable to censorship: Domain names can be blocked or restricted.\n- Limited scalability: The listed nodes are at risk of being overwhelmed by receiving all queries. Also, operators must provide their `ENR` to the domain owner for listing.\n"},{"id":"messaging/concepts/understand-peer-discovery/about-discv5","title":"About Discv5","summary":"Understand how Discv5 uses a distributed hash table to find peers across the network.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/discv5","markdown_url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/discv5.md","permalink":"/messaging/concepts/understand-peer-discovery/discv5","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-peer-discovery/about-discv5.md","headings":[{"depth":2,"text":"How Discv5 works"},{"depth":2,"text":"Pros and cons"}],"markdown":"# About Discv5\n\n#### Understand how Discv5 uses a distributed hash table to find peers across the network.\n\n`Discv5` is a decentralised and efficient [peer discovery](https://docs.logos.co/get-started/glossary.md#peer-discovery) mechanism for the [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) Network. It uses a [Distributed Hash Table (DHT)](https://en.wikipedia.org/wiki/Distributed_hash_table) for storing `ENR` records, providing resistance to censorship. `Discv5` offers a global view of participating nodes, enabling random sampling for load distribution. It uses bootstrap nodes as an entry point to the network, providing randomised sets of nodes for mesh expansion. Have a look at the [Discv5](https://lip.logos.co/messaging/core/draft/33/discv5.html) specification to learn more.\n\n## How Discv5 works\n\n```mermaid\nsequenceDiagram\n    Alice->>DNS Server: (1) Execute DNS Discovery\n    DNS Server-->>Alice: (2) Bob's connection details (UDP + TCP)\n    activate Alice\n    Alice->>Bob: (3) Execute Discv5 over UDP\n    Bob-->>Alice: (4) ENRs (UDP + TCP): Carol, David, Erin\n    Alice->>Alice: (5) Decode ENRs\n    Alice->>Carol: (6) Execute Discv5 over UDP\n    Carol-->>Alice: (7) ENRs (UDP + TCP): Frank, Gwen, Harry\n    Alice->>Alice: (8) Decode ENRs\n    deactivate Alice\n    Alice->>Alice: (9) Select peers to dial\n    Alice->>David: (10) Libp2p dial (TCP)\n    Alice->>Frank: (10) Libp2p dial (TCP)\n    Alice->>Gwen: (10) Libp2p dial (TCP)\n```\n\n1. [DNS Discovery](https://docs.logos.co/get-started/glossary.md#dns-discovery) protocol is executed.\n1. Alice retrieves Bob's [ENR](https://docs.logos.co/get-started/glossary.md#enr) (Ethereum Node Record) from DNS Server.\n1. Alice executes the Discv5 protocol with Bob using UDP connection details from ENR.\n1. Bob returns Carol's, David's and Erin's ENRs to Alice.\n1. Alice decodes ENRs and extracts the details of Carol, David, and Erin's TCP and UDP connections.\n1. Alice executes the Discv5 protocol with Carol using UDP connection details from ENR.\n1. Carol returns Frank's, Gwen's and Harry's ENRs to Alice.\n1. Alice decodes ENRs and extracts Frank's, Gwen's and Harry's TCP and UDP connection details.\n1. Alice selects to dial David, Frank and Gwen.\n1. Alice dials David, Frank and Gwen over libp2p using TCP connection details from ENRs.\n\n## Pros and cons\n\nPros:\n\n- Decentralised with random sampling from a global view.\n- Continuously researched and improved.\n\nCons:\n\n- Requires lots of connections and involves frequent churn.\n- Relies on User Datagram Protocol (UDP), which is not supported in web browsers.\n"},{"id":"messaging/concepts/understand-peer-discovery/about-peer-exchange","title":"About peer exchange","summary":"Understand how light nodes request peers from other nodes without relying on Discv5.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-exchange","markdown_url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-exchange.md","permalink":"/messaging/concepts/understand-peer-discovery/peer-exchange","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-peer-discovery/about-peer-exchange.md","headings":[{"depth":2,"text":"How peer exchange works"},{"depth":2,"text":"Pros and cons"}],"markdown":"# About peer exchange\n\n#### Understand how light nodes request peers from other nodes without relying on Discv5.\n\nThe primary objective of this protocol is to provide peer connectivity for resource-limited devices without relying on `Discv5`. The [peer exchange](https://docs.logos.co/get-started/glossary.md#peer-exchange) protocol enables [light nodes](https://docs.logos.co/get-started/glossary.md#light-node) to request peers from other nodes within the network.\n\n> **Info**\n>\n> `Peer Exchange` enables requesting random peers from other network nodes without revealing information about their connectivity or neighbourhood.\n\n## How peer exchange works\n\n```mermaid\nsequenceDiagram\n    Alice->>DNS Server: (1) Execute DNS Discovery\n    DNS Server-->>Alice: (2) Bob's multiaddr (websocket)\n    Alice-->>Bob: (3) Dial\n    Alice->>Bob: (4) Peer Exchange Query\n    Bob-->>Alice: (5) Carol's ENR, David's ENR\n    Alice->>Alice: (6) Decode ENRs\n    Alice->>Carol: (7) Dial\n    Alice->>David: (7) Dial\n    Alice-->>Bob: (8) Disconnect\n```\n\n1. [DNS Discovery](https://docs.logos.co/get-started/glossary.md#dns-discovery) protocol is executed.\n1. Alice retrieves Bob's websocket multiaddr from DNS Server.\n1. Alice dials Bob using libp2p protocols.\n1. Alice executes a Peer Exchange query to Bob.\n1. Bob returns Carol's and David's [ENR](https://docs.logos.co/get-started/glossary.md#enr) to Alice.\n1. Alice decodes ENRs and extracts Carol's and David's websocket multiaddrs.\n1. Alice dials Carol and David.\n1. Alice can now drop the connection with Bob (bootstrap node); Alice has 2 connections to the [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) Network.\n\n## Pros and cons\n\nPros:\n\n- Low resource requirements.\n- Decentralised with random sampling of nodes from a global view using `Discv5`.\n\nCons:\n\n- Decreased anonymity.\n- Imposes additional load on responder nodes.\n"},{"id":"messaging/concepts/understand-logos-delivery-security-features","title":"Understand Logos Delivery security features","summary":"See the services that keep Logos Delivery communication private and abuse-resistant.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/security-features","markdown_url":"https://docs.logos.co/messaging/concepts/security-features.md","permalink":"/messaging/concepts/security-features","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-logos-delivery-security-features.md","headings":[{"depth":2,"text":"Pseudonymity"},{"depth":2,"text":"Anonymity/unlinkability"},{"depth":2,"text":"Spam protection"},{"depth":2,"text":"Data confidentiality, integrity, and authenticity"},{"depth":2,"text":"Security considerations"}],"markdown":"# Understand Logos Delivery security features\n\n#### See the services that keep Logos Delivery communication private and abuse-resistant.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery)'s protocol layers offer different services and security considerations, shaping the overall security of Logos Delivery. We document the security models in the [RFCs of the protocols](https://rfc.vac.dev/), aiming to provide transparent and open-source references. This empowers Logos Delivery users to understand each protocol's security guarantees and limitations.\n\n## Pseudonymity\n\nLogos Delivery ensures [pseudonymity](https://lip.logos.co/messaging/core/draft/10/waku2.html#pseudonymity) across its protocol layers, using libp2p `PeerID` as identifiers instead of disclosing true identities. However, it is important to note that pseudonymity does not provide complete anonymity. Actions performed under the same pseudonym (`PeerID`) can be linked, leading to the potential re-identification of the actual actor.\n\n## Anonymity/unlinkability\n\n[Anonymity](https://lip.logos.co/messaging/core/draft/10/waku2.html#anonymity--unlinkability) means it is computationally infeasible for an adversary to connect an actor to their actions or data. To achieve anonymity, avoiding linking activities with actors or their Personally Identifiable Information (PII) is crucial. In Logos Delivery, the following anonymity features are provided:\n\n- [Publisher-message unlinkability](https://lip.logos.co/messaging/core/stable/11/relay.html#security-analysis): Makes it computationally infeasible for the publisher of messages in the `Relay` protocol to be linked to their published messages.\n- [Subscriber-topic unlinkability](https://lip.logos.co/messaging/core/stable/11/relay.html#security-analysis): Makes it computationally infeasible for the subscriber of topics in the `Relay` protocol to be linked to the topics they have subscribed to.\n\n## Spam protection\n\nThe [spam protection](https://lip.logos.co/messaging/core/draft/10/waku2.html#spam-protection) feature in `Relay` ensures that no adversary can flood the system with many messages, intentionally or not, regardless of the content's validity or usefulness. This protection is achieved through the [scoring mechanism](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#spam-protection-measures) of `GossipSub v1.1`. Peers assign scores to their connections based on their behaviour and remove peers with low scores.\n\nOngoing research is being conducted, including developing [Rate Limit Nullifiers (RLN)](https://docs.logos.co/messaging/concepts/protocols.md#rln-relay), which can be explored further at: [https://github.com/vacp2p/research/issues/148](https://github.com/vacp2p/research/issues/148).\n\n## Data confidentiality, integrity, and authenticity\n\n[Confidentiality](https://lip.logos.co/messaging/core/draft/10/waku2.html#data-confidentiality-integrity-and-authenticity) in Logos Delivery is ensured through data encryption, while integrity and authenticity are achieved through digital signatures. These security measures are available in [Waku Message (version 1)](https://lip.logos.co/messaging/core/stable/14/message.html#payload-encryption) and Noise protocols, which offer payload encryption and encrypted signatures.\n\n## Security considerations\n\nIn protocols like `Store` and `Filter`, where direct connections are required for the designated service, anonymity or unlinkability is not guaranteed. This is because nodes use their `PeerID` to identify each other during direct connections, making the service obtained in these protocols linkable to the beneficiary's `PeerID`, considered Personally Identifiable Information (PII). In `Store`, the queried node can link the querying node's `PeerID` to the topics being queried. Similarly, in `Filter`, a node can link the `PeerID` of a [light node](https://docs.logos.co/get-started/glossary.md#light-node) to its content filter. Check out the [Security considerations](https://lip.logos.co/messaging/core/draft/10/waku2.html#security-considerations) section in the Waku2 RFC for more details.\n"},{"id":"messaging/concepts/learn-the-differences-between-logos-delivery-and-libp2p","title":"Learn the differences between Logos Delivery and libp2p","summary":"Understand what Logos Delivery adds on top of libp2p and where the two differ.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/logos-delivery-and-libp2p","markdown_url":"https://docs.logos.co/messaging/concepts/logos-delivery-and-libp2p.md","permalink":"/messaging/concepts/logos-delivery-and-libp2p","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/learn-the-differences-between-logos-delivery-and-libp2p.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"Logos Delivery as an open network"},{"depth":2,"text":"Logos Delivery as a turnkey solution"},{"depth":2,"text":"Economic spam protection"}],"markdown":"# Learn the differences between Logos Delivery and libp2p\n\n#### Understand what Logos Delivery adds on top of libp2p and where the two differ.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nSince [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) is built on top of libp2p, they share a lot of concepts and terminologies between them. However, there are key differences between them that are worth noting.\n\n## The basics\n\n- Logos Delivery is built on top of libp2p, so the two share many concepts and terms.\n- Unlike libp2p, Logos Delivery is an [open network](#logos-delivery-as-an-open-network) and a [turnkey solution](#logos-delivery-as-a-turnkey-solution).\n- Logos Delivery adds [economic spam protection](#economic-spam-protection), which libp2p does not provide.\n\n## Logos Delivery as an open network\n\nLogos Delivery intends to incentivise mechanisms to run nodes, but it is not part of libp2p's scope. Additionally, users and developers can use Logos Delivery without deploying their own infrastructure, though anyone can choose to [run a node](https://docs.logos.co/messaging/delivery/run-delivery-node-from-cli.md) to help decentralise the network.\n\n## Logos Delivery as a turnkey solution\n\nLogos Delivery includes various protocols covering the following domains: privacy preservation, censorship resistance, and platform agnosticism, allowing it to run on any platform or environment.\n\nLogos Delivery provides out-of-the-box protocols to enable [mostly offline](https://docs.logos.co/get-started/glossary.md#mostly-offline)/resource-limited devices, [Store](https://docs.logos.co/messaging/concepts/protocols.md#store)/[Light Push](https://docs.logos.co/messaging/concepts/protocols.md#light-push)/[Filter](https://docs.logos.co/messaging/concepts/protocols.md#filter) caters to those use cases.\n\n## Economic spam protection\n\nlibp2p does not have strong spam protection guarantees, [RLN Relay](https://docs.logos.co/messaging/concepts/protocols.md#rln-relay) is a protocol being developed by the Logos Delivery team towards this goal.\n"},{"id":"messaging/delivery/use-delivery-module-api-in-app","title":"Use the Delivery module API in an app","summary":"Get started integrating Logos messaging into a C++ module.","section":"messaging","doc_type":"procedure","url":"https://docs.logos.co/messaging/delivery/use-delivery-module-api-in-app","markdown_url":"https://docs.logos.co/messaging/delivery/use-delivery-module-api-in-app.md","permalink":"/messaging/delivery/use-delivery-module-api-in-app","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/delivery/use-delivery-module-api-in-app.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Create a Logos module"},{"depth":2,"text":"Step 2: Declare `delivery_module` as a dependency"},{"depth":2,"text":"Step 3: Subscribe to events in your backend"},{"depth":2,"text":"Step 4: Create and start the node"},{"depth":2,"text":"Step 5: Use the Messaging API"},{"depth":2,"text":"Step 6: Use the Reliable Channels API"},{"depth":2,"text":"Step 7: Shut down"},{"depth":2,"text":"Step 8: Build and run"},{"depth":2,"text":"Step 9: Verify a two-instance message exchange"},{"depth":2,"text":"Step 10: Verify a two-instance reliable channel exchange"},{"depth":2,"text":"Troubleshooting the Logos Delivery module"},{"depth":3,"text":"`createNode` returns an unsuccessful `LogosResult`?"},{"depth":3,"text":"`send()` returns an unsuccessful `LogosResult`?"},{"depth":3,"text":"`messageSent` never fires after a successful `send()`?"},{"depth":3,"text":"`messageReceived` never fires?"},{"depth":3,"text":"The node process dies when I re-open a channel I closed?"},{"depth":3,"text":"`channelCreate` returns `channel already exists`?"},{"depth":3,"text":"`channelMessageReceived` never fires on the other participant?"},{"depth":3,"text":"`channelMessageReceived` fires but the payload is empty?"},{"depth":3,"text":"`channelSend` returns `no reliable channel manager`?"},{"depth":3,"text":"Two instances of the same app on one host fail to start?"},{"depth":3,"text":"`createNode` succeeds but a second call without `stop()` causes undefined behaviour?"}],"markdown":"# Use the Delivery module API in an app\n\n#### Get started integrating Logos messaging into a C++ module.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.2.1**.\n\nThis procedure covers building a Logos [module](https://docs.logos.co/get-started/glossary.md#module) that calls the [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) API to subscribe to content topics, send messages, react to delivery events, and exchange messages over a reliable channel. It gives application developers a working pattern for integrating Logos messaging into their C++ modules. A complete, runnable reference implementation is available in [`logos-delivery-demo`](https://github.com/logos-co/logos-delivery-demo/tree/v0.2.1) (tag `v0.2.1`).\n\nThe two repositories used in this tutorial are [`logos-delivery-module`](https://github.com/logos-co/logos-delivery-module) (pinned to [`v0.2.1`](https://github.com/logos-co/logos-delivery-module/tree/b8b9ac2f4667bc63644b2116f64a07aa30cfd3ef)) and [`logos-delivery`](https://github.com/logos-messaging/logos-delivery), which is a transitive dependency resolved and linked statically by Nix.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux: aarch64 or x86_64\n>     - macOS: aarch64 or x86_64\n> - ~1 GB of RAM\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>     ```bash\n>     mkdir -p ~/.config/nix\n>     echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>     ```\n\n## What to expect\n\n- You can subscribe to a [content topic](https://docs.logos.co/get-started/glossary.md#content-topic) and receive messages.\n- You can send a message and confirm delivery by tracking `messageSent` and `messagePropagated` events.\n- You can open a reliable channel and exchange messages on it with automatic retransmission and acknowledgements.\n- You can integrate the full Logos Delivery lifecycle—create, start, subscribe, send, stop—into your C++ module.\n\n## Step 1: Create a Logos module\n\nScaffold a new module using [`logos-module-builder`](https://github.com/logos-co/logos-module-builder). For a full walkthrough, see the [Build a Logos C++ UI module](https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module.md) tutorial.\n\n1. Create and enter the project directory:\n\n   ```bash\n   mkdir your-module-name && cd your-module-name\n   ```\n\n1. Initialise from the template:\n\n   ```bash\n   nix flake init -t github:logos-co/logos-module-builder/0.2.5#ui-qml-backend\n   ```\n\n1. Initialise a Git repository and stage all generated files:\n\n   ```bash\n   git init && git add -A\n   ```\n\n   The scaffold builds as it stands. It gives you:\n   - `src/ui_example.rep` - the view contract;\n   - `src/ui_example_backend.{h,cpp}` - the class you write;\n   - `src/qml/Main.qml` - your module's UI entry point.\n\n   > **Info**\n   >\n   > You are free to rename the files and classes later. In this doc we'll refer to them as generated by default.\n\n## Step 2: Declare `delivery_module` as a dependency\n\nAdd `delivery_module` to both `metadata.json` and `flake.nix`, pinning to the released tag so your app stays stable as the module API evolves.\n\n> **Info**\n>\n> The flake input name (`delivery_module`) must exactly match the dependency name in `metadata.json`. `logos-module-builder` uses this name to generate the typed wrapper at build time.\n\n1. In `metadata.json`, add `delivery_module` to the dependencies array:\n\n   ```json\n   {\n     \"dependencies\": [\"delivery_module\"],\n     ...\n   }\n   ```\n\n1. In `flake.nix`, add a matching pinned input:\n\n   ```nix\n   inputs = {\n     logos-module-builder.url = \"github:logos-co/logos-module-builder/0.2.5\";\n     delivery_module = {\n       url = \"github:logos-co/logos-delivery-module/v0.2.1\";\n       # Your module and delivery_module must be built by the same\n       # logos-module-builder: the event emitter and the consumer have to agree\n       # on the binary event wire format.\n       inputs.logos-module-builder.follows = \"logos-module-builder\";\n     };\n   };\n   ```\n\n   > **Warning**\n   >\n   > Use `logos-module-builder` 0.2.5 or newer. On older builders, binary event payloads (`messageReceived`, `channelMessageReceived`) arrive empty.\n\n## Step 3: Subscribe to events in your backend\n\nAdd `onContextReady()` to the backend the template scaffolded, and arm the delivery subscriptions there. \n\n1. Override `onContextReady()` in `ui_example_backend.h`:\n\n   ```cpp\n   #pragma once\n\n   #include \"rep_ui_example_source.h\"\n   #include \"logos_ui_plugin_context.h\"\n\n   class UiExampleBackend : public UiExampleSimpleSource,\n                            public LogosUiPluginContext\n   {\n   public:\n       int add(int a, int b) override;\n\n       void onContextReady() override;\n   };\n   ```\n\n1. Arm the node and Messaging API subscriptions in `ui_example_backend.cpp`:\n\n   ```cpp\n   #include \"ui_example_backend.h\"\n   #include \"logos_sdk.h\"   // generated umbrella — exposes modules() with your module's dependency: `.delivery_module`\n\n   void UiExampleBackend::onContextReady()\n   {\n       auto& delivery = modules().delivery_module;\n\n       delivery.onConnectionStateChanged([](QString status, qint64 timestamp) {});\n\n       delivery.onMessageReceived(\n           [this](QString messageHash, QString contentTopic, QByteArray payload, qint64 timestamp) {\n               // payload is raw bytes, not text.\n           });\n\n       delivery.onMessageSent([](QString requestId, QString messageHash, qint64 timestamp) {});\n       delivery.onMessagePropagated([](QString requestId, QString messageHash, qint64 timestamp) {});\n       delivery.onMessageError(\n           [](QString requestId, QString messageHash, QString error, qint64 timestamp) {});\n   ```\n\n1. Arm the Reliable Channels subscriptions too, if your module uses channels (Step 6):\n\n   ```cpp\n       delivery.onChannelMessageReceived(\n           [this](QString channelId, QString senderId, QByteArray payload, qint64 timestamp) {\n           });\n\n       delivery.onChannelMessageSent([](QString channelId, QString requestId, qint64 timestamp) {});\n       delivery.onChannelMessageError(\n           [](QString channelId, QString requestId, QString error, qint64 timestamp) {});\n   }\n   ```\n\n## Step 4: Create and start the node\n\n`createNode` builds the node from a JSON configuration and `start()` connects it to the network. Both are synchronous and return a `LogosResult`—always check `success` before continuing and surface `getError()` on failure.\n\n> **Info**\n>\n> The node is a singleton per Logos Core instance: call `createNode` exactly once per context, and expect the node to already exist when another module created it first.\n\n1. Initialise the node with `createNode`. For a complete list of node configuration keys, see [`createNode`](https://logos-co.github.io/logos-delivery-module/v0.2.1/pages/api_reference.html#_CPPv4N18DeliveryModuleImpl10createNodeERKNSt6stringE) in the Delivery module API reference.\n\n   ```cpp\n   auto& delivery = modules().delivery_module;\n\n   // \"mode\": \"Core\" (relay node) or \"Edge\" (light node).\n   // \"preset\": the network to join — \"logos.test\" or \"logos.dev\"\n   LogosResult r = delivery.createNode(R\"({\"mode\":\"Core\",\"preset\":\"logos.test\"})\");\n   if (!r.success) {\n       qWarning() << \"createNode failed:\" << r.getError();\n       return;\n   }\n   ```\n\n   > **Warning**\n   >\n   > Keep the config to `mode`, `preset`, `messagingOverrides`, and `channelsOverrides`. Any other top-level key (`logLevel`, for example) selects the legacy flat configuration shape.\n\n1. Connect to the network with `start()`. The `connectionStateChanged` event fires once the node connects to peers:\n\n   ```cpp\n   r = delivery.start();\n   if (!r.success) {\n       qWarning() << \"start failed:\" << r.getError();\n       return;\n   }\n   ```\n\n   The node is not connected the moment `start()` returns. Wait for `connectionStateChanged` before sending on either API.\n\n## Step 5: Use the Messaging API\n\nThe Messaging API is publish/subscribe on a [content topic](https://docs.logos.co/get-started/glossary.md#content-topic): you subscribe to the topics you care about and send raw payloads to them.\n\n1. Subscribe to a content topic using a [LIP-23](https://lip.logos.co/messaging/informational/draft/23/topics.html) content-topic string. Call `subscribe()` before any messages are sent on that topic:\n\n   ```cpp\n   LogosResult r = delivery.subscribe(contentTopic);\n   if (!r.success) {\n       qWarning() << \"subscribe failed:\" << r.getError();\n   }\n   ```\n\n1. Send a message. On success, `getString()` returns the request ID; track it through `messagePropagated` → `messageSent` events (or `messageError`):\n\n   ```cpp\n   // payload is a QByteArray of raw bytes, the same type messageReceived delivers.\n   LogosResult r = delivery.send(contentTopic, payload);\n   if (!r.success) {\n       qWarning() << \"send failed:\" << r.getError();\n       return;\n   }\n   const QString requestId = r.getString();\n   ```\n\n1. Receive messages through the `onMessageReceived` subscription you armed in Step 3. It fires for every message on a subscribed topic, including messages sent by you.\n\n1. Stop receiving on a topic with `delivery.unsubscribe(contentTopic)`. Subscriptions are node-wide, so unsubscribing a topic another module subscribed to stops its traffic too.\n\n## Step 6: Use the Reliable Channels API\n\nReliable Channels add end-to-end reliability on top of the Messaging API: it tracks acknowledgements with [SDS](https://lip.logos.co/anoncomms/raw/sds.html) and retransmits what peers did not acknowledge.\n\n> **Warning**\n>\n> The Reliable Channels API is a **Developer Preview**. It is not feature-complete and is still under testing—expect gaps in behaviour, and expect the API surface to change before general availability.\n\nA channel is addressed by an application-chosen `channelId`, so you never hold a channel object—every call takes the id. All four calls are synchronous and return a `LogosResult`; delivery outcomes arrive through the channel subscriptions from Step 3.\n\n> **Info**\n>\n> Reliable channels need the full stack, which is what `createNode` mounts by default (`\"entryLayer\": \"channels\"`). On a kernel-only node, every `channel*` call fails with `no reliable channel manager`.\n\n1. Create a channel with `channelCreate()`. Every participant creates the channel with the same `channelId` and `contentTopic`, and its own `senderId`:\n\n   ```cpp\n   // senderId: this participant's SDS identifier — any string unique per participant\n   LogosResult r = delivery.channelCreate(channelId, contentTopic, senderId);\n   if (!r.success) {\n       qWarning() << \"channelCreate failed:\" << r.getError();\n       return;\n   }\n   ```\n\n   Creating a channel subscribes its content topic for you—you don't call `subscribe()` for a channel topic.\n\n1. Check whether a channel is currently open.\n\n   ```cpp\n   LogosResult r = delivery.channelExists(channelId);\n   const bool open = r.success && r.getString() == QLatin1String(\"true\");\n   ```\n\n1. Send on the channel. `channelSend()` returns a request ID—track it through `channelMessageSent` or `channelMessageError`.\n\n   ```cpp\n   // payload is a QByteArray of raw bytes, same as channelMessageReceived delivers.\n   LogosResult r = delivery.channelSend(channelId, payload);\n   if (!r.success) {\n       qWarning() << \"channelSend failed:\" << r.getError();\n       return;\n   }\n   const QString requestId = r.getString();\n   ```\n\n1. Receive channel messages through the `onChannelMessageReceived` subscription. Unlike `onMessageReceived`, it fires only for the other participants' messages—your own channel sends come back as `channelMessageSent`, not as a received message.\n\n1. Close the channel when you are done with it. This stops the channel's SDS loops and unsubscribes its content topic, unless another open channel still uses that topic:\n\n   ```cpp\n   LogosResult r = delivery.channelClose(channelId);\n   if (!r.success) {\n       qWarning() << \"channelClose failed:\" << r.getError();\n   }\n   ```\n\nTo tune reliability, pass a `channelsOverrides` object in the `createNode` config of Step 4. Unset keys keep the defaults shown here:\n\n```json\n{\n  \"mode\": \"Core\",\n  \"preset\": \"logos.test\",\n  \"channelsOverrides\": {\n    \"sdsAcknowledgementTimeoutMs\": 5000,\n    \"sdsMaxRetransmissions\": 5,\n    \"sdsCausalHistorySize\": 2\n  }\n}\n```\n\n> **Warning**\n>\n> Reliable channels give you delivery reliability, not confidentiality: channel payloads travel unencrypted unless your application encrypts them. For end-to-end encrypted conversations, use the [Logos Chat module](https://docs.logos.co/messaging/chat-module/build-logos-module-that-uses-chat-module-api.md).\n\n## Step 7: Shut down\n\nShut down cleanly with `stop()`. This tears down the underlying node and drops every active subscription, channel, and event listener:\n\n```cpp\nLogosResult r = delivery.stop();\nif (!r.success) {\n    qWarning() << \"stop failed:\" << r.getError();\n}\n```\n\nBecause the node is shared by every module on the host, `stop()` also ends the traffic of any other module using it.\n\n## Step 8: Build and run\n\n1. Build the module:\n\n   ```sh\n   nix build\n   ```\n\n1. Preview the module using `logos-standalone-app` (for `ui_qml` modules):\n\n   ```sh\n   nix run\n   ```\n\n1. Package as `.lgx` for installation into `logos-basecamp`:\n\n   ```sh\n   nix build .#lgx\n   ```\n\n## Step 9: Verify a two-instance message exchange\n\nMessaging is only proven end to end when a payload travels between two running nodes. Start two copies of your module, each with its own session directory:\n\n```sh\n# terminal A\nnix run . -- --user-dir ~/.local/share/ui_example_a\n# terminal B\nnix run . -- --user-dir ~/.local/share/ui_example_b\n```\n\n1. In both instances, run `createNode` with the same preset, then `start()`, and wait until each reports a connected state through `connectionStateChanged`.\n1. In both instances, call `subscribe(contentTopic)` on the same content topic.\n1. In instance A, call `send(contentTopic, payload)`. A's `messagePropagated` and `messageSent` events fire, carrying the request id the call returned.\n1. Instance B fires `messageReceived` with the same payload bytes on that content topic.\n1. Send in the other direction and confirm instance A receives it.\n\nA `messageReceived` event on the instance that did not send confirms the whole path: node creation, network connection, subscription, and publish.\n\n## Step 10: Verify a two-instance reliable channel exchange\n\nReliability shows only between two participants of the same channel. Use the same two instances as in Step 9, both connected.\n\n1. In both instances, call `channelCreate(channelId, contentTopic, senderId)` with the same `channelId` and `contentTopic`, and a `senderId` unique to each instance.\n1. In instance A, call `channelSend(channelId, payload)`. A's `channelMessageSent` event fires once the send is confirmed, carrying the request id the call returned.\n1. Instance B fires `channelMessageReceived` with A's `senderId` and the payload bytes.\n1. Send from instance B and confirm instance A receives it, attributed to B's `senderId`.\n\nMessages arriving on both sides, each attributed to the sending participant, confirms the channel journey: channel creation on a shared content topic and acknowledgement through SDS.\n\n> **Tip**\n>\n> [`logos-delivery-demo`](https://github.com/logos-co/logos-delivery-demo/tree/v0.2.1) drives both verifications from its UI: every API call is a button, and its event log renders every event described above.\n\n## Troubleshooting the Logos Delivery module\n\n### `createNode` returns an unsuccessful `LogosResult`?\n\nThe JSON may be malformed, or an internal initialisation error occurred. Validate that the JSON is well-formed and that key names are camelCase. Set `\"messagingOverrides\": {\"logLevel\": \"DEBUG\"}` for verbose output and inspect `result.getError()` for details.\n\n### `send()` returns an unsuccessful `LogosResult`?\n\nThe node was not started, or `contentTopic` is empty or invalid. Call `start()` first and verify it returned success. Confirm the content topic follows the [LIP](https://docs.logos.co/get-started/glossary.md#lip)-23 format.\n\n### `messageSent` never fires after a successful `send()`?\n\nThe node may not be connected to peers yet, or the network layer rejected the message (for example, an RLN proof failure). Wait for `connectionStateChanged` before sending. If `messageError` fires, its `error` argument carries the reason.\n\n### `messageReceived` never fires?\n\n`subscribe()` was not called before messages were sent, or the payload was sent on a different content topic. Call `subscribe(topic)` before any messages are sent on that topic.\n\n### The node process dies when I re-open a channel I closed?\n\nKnown issue in this release: ([logos-delivery#4116](https://github.com/logos-messaging/logos-delivery/issues/4116)). Calling `channelCreate` with the id of a channel you closed with `channelClose` crashes the delivery node, if that channel had received a message from a peer. The module reports the call as failed and the node is gone—every later call fails until the module is reloaded and the node re-created. Use a fresh channel id rather than re-opening a closed one.\n\n### `channelCreate` returns `channel already exists`?\n\nThe channel id is already open on this node—channel ids are node-wide, so another module may hold it. Call `channelExists(channelId)` first, and reuse the open channel or pick an application-specific id.\n\n### `channelMessageReceived` never fires on the other participant?\n\nThe participants disagree on the channel: both sides must call `channelCreate()` with the same `channelId` and the same `contentTopic`, each with its own `senderId`. Reusing one `senderId` for two participants breaks SDS bookkeeping.\n\n### `channelMessageReceived` fires but the payload is empty?\n\nYour module was built with a `logos-module-builder` older than 0.2.5, where binary event payloads arrive empty. Pin `logos-module-builder` to 0.2.5 or newer and make `delivery_module` follow the same input, as in Step 2.\n\n### `channelSend` returns `no reliable channel manager`?\n\nThe node was created as kernel-only (`\"entryLayer\": \"kernel\"`). Reliable channels need the default full stack—omit `entryLayer`, or set it to `\"channels\"`.\n\n### Two instances of the same app on one host fail to start?\n\nBoth instances bound the same port. With the layered config (`mode`, `preset`, and the `*Overrides` objects only), ports default to `0` and the OS assigns a free one per instance. Adding any other top-level key switches the config to the legacy flat shape, whose kernel defaults pin port `60000`. Keep the config layered, or assign distinct explicit port values per instance.\n\n### `createNode` succeeds but a second call without `stop()` causes undefined behaviour?\n\n`createNode` must be called exactly once per context. Call `stop()` and destroy the context before calling `createNode` again.\n"},{"id":"messaging/delivery/run-delivery-node-from-cli","title":"Run a Delivery node from the CLI","summary":"Use the Delivery module to run a Logos Delivery node on the CLI","section":"messaging","doc_type":"procedure","url":"https://docs.logos.co/messaging/delivery/run-delivery-node-from-cli","markdown_url":"https://docs.logos.co/messaging/delivery/run-delivery-node-from-cli.md","permalink":"/messaging/delivery/run-delivery-node-from-cli","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/delivery/run-delivery-node-from-cli.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Install and start the daemon"},{"depth":2,"text":"Step 2: Load the module and boot the node"},{"depth":2,"text":"Step 3: Verify the node is running"},{"depth":2,"text":"Troubleshooting delivery node setup"},{"depth":3,"text":"Why does `logosctl call` return an error after `module load`?"}],"markdown":"# Run a Delivery node from the CLI\n\n#### Use the Delivery module to run a Logos Delivery node on the CLI\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure covers how to install and start a `logos-delivery-module` node connected to the Logos testnet (v0.3). It is intended for node operators who want to run their own Delivery node and join the network.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>    - Linux\n>    - macOS\n> - Network access so both node instances can reach each other.\n> - Inbound ports open:\n>    - TCP `30303`: P2P (`tcpPort`).\n>    - UDP `30303`: P2P over QUIC, on by default at the `tcpPort` number.\n>    - UDP `9000`: discv5 discovery (`discv5UdpPort`).\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n> - `curl` and a shell\n\n## What to expect\n\n- You can start a `delivery_module` node connected to the Logos Network.\n- You can confirm the node is running and has a live network identity by querying its discv5 [ENR](https://docs.logos.co/get-started/glossary.md#enr).\n- You can configure the node for a different network preset by swapping the config file passed to `createNode`.\n\n## Step 1: Install and start the daemon\n\n1. Start `logosctl`:\n\n   ```sh\n   logosctl daemon start\n   ```\n\n1. In a new terminal window with the same user, refresh the official module catalogue:\n\n   ```sh\n   logosctl catalog refresh\n   ```\n\n1. Download and install the [delivery module](https://docs.logos.co/get-started/glossary.md#delivery-module):\n\n   ```bash\n   logosctl package install delivery_module \\\n   --version 0.3.0 \\\n   --yes\n   ```\n\n   > **Note**\n   >\n   > Individual module [package](https://docs.logos.co/get-started/glossary.md#package) versions (for example, delivery module version 0.3.0) are pinned independently and do not necessarily match the testnet version number (0.3.0).\n\n1. Write the testnet config:\n\n   ```bash\n   cat > logos-test.json <<EOF\n   {\n      \"entryLayer\": \"kernel\",\n      \"kernelConf\": {\n         \"preset\": \"logos.test\",\n         \"relay\": true,\n         \"logLevel\": \"INFO\",\n         \"tcpPort\": 30303,\n         \"discv5UdpPort\": 9000,\n         \"discv5Discovery\": true,\n         \"nat\": \"extip:<public-ip>\"\n      }\n   }\n   EOF\n   ```\n\n   - `logos-test.json` includes the following fields:\n\n   | Field | Purpose |\n   |-------|---------|\n   | `entryLayer` | Delivery stack layer; use `kernel` for a node-operator service |\n   | `kernelConf` | Kernel node configuration |\n   | `kernelConf.preset` | Network preset |\n   | `kernelConf.relay` | Enable the [Relay](https://docs.logos.co/get-started/glossary.md#relay) protocol |\n   | `kernelConf.logLevel` | Log verbosity |\n   | `kernelConf.tcpPort` | Public TCP P2P port |\n   | `kernelConf.discv5UdpPort` | Public UDP discovery port |\n   | `kernelConf.discv5Discovery` | Enable discv5 discovery |\n   | `kernelConf.nat` | Public IP advertisement mode |\n\n## Step 2: Load the module and boot the node\n\nRun these commands for your path.\n\n1. Load the delivery module:\n\n   ```bash\n   logosctl module load delivery_module\n   ```\n\n1. Create the node with the testnet config:\n\n   ```bash\n   logosctl call delivery_module createNode @logos-test.json\n   ```\n\n1. Start the node:\n\n   ```bash\n   logosctl call delivery_module start\n   ```\n\n## Step 3: Verify the node is running\n\nQuery the node's discv5 ENR to confirm it booted with a network identity and joined the Logos Testnet.\n\n1. Verify the status of the node:\n\n   ```bash\n   logosctl daemon status\n   ```\n\n1. Run the [health query](https://github.com/logos-co/logos-delivery-module/blob/master/docs/pages/query-node.md):\n\n   ```bash\n   logosctl call delivery_module getNodeInfo MyENR --json | jq\n   ```\n\n   Expected output:\n\n   ```json\n   {\n     \"method\": \"getNodeInfo\",\n     \"module\": \"delivery_module\",\n     \"result\": {\n       \"error\": null,\n       \"success\": true,\n       \"value\": \"enr:-LW4QItc5tHj3rWoFaaQIUWvaBYijDf2TJKW83SNdyylJVAYVoUlBl1h5...\"\n     },\n     \"status\": \"ok\"\n   }\n   ```\n\n   - The `\"value\"` field is your node's live ENR and will differ from the example above.\n   - `\"success\": true` and `\"status\": \"ok\"` confirm the delivery node is running.\n\n1. Stop the node when finished:\n\n   ```bash\n   logosctl daemon stop\n   ```\n\n## Troubleshooting delivery node setup\n\n### Why does `logosctl call` return an error after `module load`?\n\nThe daemon may not have finished starting. Wait a few seconds after `logosctl daemon start` returns and retry. For Path A, confirm the container is running with `docker ps` before calling `docker exec logos-node logosctl …`.\n"},{"id":"messaging/chat-module/build-logos-module-that-uses-chat-module-api","title":"Build a Logos module that uses the Chat module API","summary":"Get started with private 1:1 and group end-to-end encrypted messaging in your own Logos module.","section":"messaging","doc_type":"procedure","url":"https://docs.logos.co/messaging/chat-module/build-logos-module-that-uses-chat-module-api","markdown_url":"https://docs.logos.co/messaging/chat-module/build-logos-module-that-uses-chat-module-api.md","permalink":"/messaging/chat-module/build-logos-module-that-uses-chat-module-api","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/chat-module/build-logos-module-that-uses-chat-module-api.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Scaffold a new Logos module"},{"depth":2,"text":"Step 2: Declare `chat_module` as a dependency"},{"depth":2,"text":"Step 3: Subscribe to `chat_module` events"},{"depth":2,"text":"Step 4: Initialise the chat client"},{"depth":2,"text":"Step 5: Direct conversations"},{"depth":2,"text":"Step 6: Group conversations"},{"depth":2,"text":"Step 7: Read state and shut down"},{"depth":2,"text":"Step 8: Build and run"},{"depth":2,"text":"Step 9: Verify a two-instance direct exchange"},{"depth":2,"text":"Step 10: Verify a three-instance group conversation"},{"depth":2,"text":"Troubleshooting the Logos Chat module"},{"depth":3,"text":"Why does a method fail?"},{"depth":3,"text":"Why do peers not connect or messages not propagate?"},{"depth":3,"text":"Why does `send_message` succeed but peers never receive the message?"},{"depth":3,"text":"Why hasn't an invited member joined the group yet?"},{"depth":3,"text":"Why is `send_message` rejected right after a membership change?"},{"depth":3,"text":"Why does a read stall the UI?"},{"depth":3,"text":"Why can't I send into a conversation after a restart?"}],"markdown":"# Build a Logos module that uses the Chat module API\n\n#### Get started with private 1:1 and group end-to-end encrypted messaging in your own Logos module.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure covers how to build a Logos [module](https://docs.logos.co/get-started/glossary.md#module) that calls the [logos-chat-module](https://github.com/logos-co/logos-chat-module) API (tag `v0.3.0`) to exchange addresses, open private 1:1 (or *direct*) and group conversations, and send and receive end-to-end encrypted messages on the Logos network. It is intended for application developers who want to integrate private messaging without taking direct dependencies on `logos-chat` or `logos-delivery`.\n\nEach launch is a new chat **installation** with a new address. Conversations and their messages are saved in the instance directory and come back after a restart, but only as read-only history: you can read them, but not send into them.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux\n>     - macOS\n> - Network access.\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>     ```bash\n>     mkdir -p ~/.config/nix\n>     echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>     ```\n> - An understanding of [Logos modules](https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module.md)\n\n## What to expect\n\n- You can initialise a chat client, connect to the Logos network, and exchange end-to-end encrypted messages with another instance.\n- You can open a private 1:1 conversation by exchanging addresses [out of band](https://docs.logos.co/get-started/glossary.md#out-of-band) and calling `create_conversation` from the initiating side.\n- You can create a group conversation with `create_group_conversation`, grow it with `add_group_member`, and exchange messages with all group members.\n- You can read back conversations and messages from earlier sessions as history.\n- You can integrate the full chat lifecycle—init, subscribe to events, share your address, open conversations, send, shut down—into any Logos C++ module.\n\n## Step 1: Scaffold a new Logos module\n\nScaffold a new module using [`logos-module-builder`](https://github.com/logos-co/logos-module-builder). For a full walkthrough, see the [Build a Logos C++ UI module](https://docs.logos.co/core/build-modules/build-a-logos-cpp-ui-module.md) tutorial.\n\n1. Create and enter the project directory:\n\n   ```bash\n   mkdir your-module-name && cd your-module-name\n   ```\n\n1. Initialise from the template:\n\n   ```bash\n   nix flake init -t github:logos-co/logos-module-builder/0.3.1#ui-qml-backend\n   ```\n\n1. Initialise a Git repository and stage all generated files:\n\n   ```bash\n   git init && git add -A\n   ```\n\n1. Keep the generated files—you build on them, you don't delete them. The template is a working `ui_example` module. Its main files are:\n\n   | File | What it is | Can you edit it? |\n   |---|---|---|\n   | `src/ui_example_backend.{h,cpp}` | The C++ backend | Yes—your chat code goes here (Steps 3–7) |\n   | `src/ui_example.rep` | The interface between the backend and the view | Later—add the slots and properties your view needs |\n   | `src/qml/Main.qml` | The example view | Later—replace with your own UI |\n   | `metadata.json`, `flake.nix` | Build config | Step 2 only |\n\n   You add your chat code in `UiExampleBackend::onContextReady()`. Leave the example `add` / `status` UI as is for now.\n\n> **Info**\n>\n> Keep the module named `ui_example`. The name is referenced in `metadata.json`, `CMakeLists.txt`, the `src/ui_example*` files, and `Main.qml` (`logos.module(\"ui_example\")`)—they must all match, or `nix build` fails. To use a different name, change it in every one of these.\n\n## Step 2: Declare `chat_module` as a dependency\n\nAdd `chat_module` to both `metadata.json` and `flake.nix`, pinning to the released tag so your app stays stable as the module's API evolves.\n\n> **Info**\n>\n> The flake input name (`chat_module`) must match the dependency name in `metadata.json`. `logos-module-builder` automatically generates the typed `chat_module` wrapper at build time.\n\n1. In `metadata.json`, replace the empty dependencies array with `chat_module` and `delivery_module`:\n\n   ```json\n   {\n     \"name\": \"ui_example\",\n     \"dependencies\": [\n       { \"name\": \"chat_module\", \"version\": \">=0.3.0\" },\n       { \"name\": \"delivery_module\", \"version\": \">=0.3.0\" }\n     ],\n     ...\n   }\n   ```\n\n1. In `flake.nix`, pin `chat_module`, take its [transport](https://docs.logos.co/get-started/glossary.md#transport) dependency `delivery_module` from it, then map the delivery input to the `delivery_module` dependency so the builder can resolve its runtime:\n\n   ```nix\n   inputs = {\n     # Follow chat_module's own builder so the codegen chain matches the one the\n     # module was released against.\n     logos-module-builder.follows = \"chat_module/logos-module-builder\";\n     # Pin chat_module to the released tag so its API can't shift under `nix flake update`.\n     chat_module.url = \"github:logos-co/logos-chat-module/v0.3.0\";\n     # Build against the exact delivery_module that chat_module v0.3.0 is built against.\n     logos-delivery-module.follows = \"chat_module/logos-delivery-module\";\n   };\n\n   outputs = inputs@{ logos-module-builder, logos-delivery-module, ... }:\n     logos-module-builder.lib.mkLogosQmlModule {\n       src = ./.;\n       configFile = ./metadata.json;\n       # Map the delivery input to the `delivery_module` dependency name.\n       flakeInputs = { delivery_module = logos-delivery-module; } // inputs;\n     };\n   ```\n\n> **Info**\n>\n> `chat_module` reaches `delivery_module` over IPC, but the builder still resolves each `metadata.json` dependency's runtime from a matching flake input. `delivery_module` therefore needs the `logos-delivery-module` input, mapped in through `flakeInputs`. Without it, `nix build` cannot resolve `delivery_module`. This mirrors how [`logos-chat-ui`](https://github.com/logos-co/logos-chat-ui/blob/v0.3.0/flake.nix) wires the two modules together.\n\n## Step 3: Subscribe to `chat_module` events\n\nThe template's backend derives `LogosUiPluginContext`, which provides `modules()`: typed calls and event subscriptions for each module in `dependencies`. `modules()` is ready only once the framework calls `onContextReady()`, so subscribe there, and subscribe before calling `init()` so you do not miss early events or the first incoming messages.\n\n1. Override `onContextReady()` in the header `src/ui_example_backend.h`:\n\n   ```cpp\n   class UiExampleBackend : public UiExampleSimpleSource,\n                            public LogosUiPluginContext\n   {\n   public:\n       int add(int a, int b) override;\n       void onContextReady() override;\n   };\n   ```\n\n1. In `src/ui_example_backend.cpp`, include the generated umbrella header and subscribe to the module's push events. Each `on<Event>` method takes a callback with the event's arguments in the order declared in `chat_module.lidl`:\n\n   ```cpp\n   #include \"logos_sdk.h\"   // generated umbrella—exposes modules().chat_module\n\n   void UiExampleBackend::onContextReady()\n   {\n       auto& chat = modules().chat_module;\n\n       // A new message arrived in a conversation. sender is the sender's account address.\n       chat.onMessage_received([](const QString& convoId, const QString& content,\n                                  qlonglong timestampMs, const QString& sender) { });\n       // One of your own messages was handed to delivery and recorded.\n       chat.onMessage_sent([](const QString& convoId, const QString& content,\n                              qlonglong timestampMs) { });\n       // A conversation was created—incoming from a peer, or your own outgoing one.\n       // kind is \"direct\" (1:1) or \"group\"; name and desc are empty for a direct one.\n       chat.onConversation_created([](const QString& convoId, bool isOutgoing,\n                                      const QString& peerLabel, const QString& kind,\n                                      const QString& name, const QString& desc) { });\n       chat.onConversation_updated([](const QString& convoId) { });\n       chat.onMembers_changed([](const QString& convoId) { });\n       chat.onConversation_deleted([](const QString& convoId) { });\n       // Delivery/connection state changed—drives your \"connected\" indicator.\n       // state is \"initialising\" | \"online\" | \"stopped\" | \"error\".\n       chat.onDelivery_state_changed([](const QString& state, const QString& detail,\n                                        bool deliveryAdopted) { });\n\n       // Step 4 continues here: init().\n   }\n   ```\n\n   See the [Chat module API reference](https://logos-co.github.io/logos-chat-module/v0.3.0/pages/api_reference.html) for the exact argument list of every method and event. It is generated from [`rust-lib/chat_module.lidl`](https://github.com/logos-co/logos-chat-module/blob/v0.3.0/rust-lib/chat_module.lidl), the contract this tutorial's client is built from.\n\n## Step 4: Initialise the chat client\n\nStatus-bearing methods return their result **synchronously** as a `LogosResult`.\n- `res.success` tells you whether the call succeeded;\n- `res.getError<QString>()` carries the failure reason;\n- `res.getValue<QString>()` carries the returned value (for example, the new conversation's id).\n\nOngoing activity—incoming messages, new conversations, delivery-state changes—arrives **asynchronously** through the push events you subscribed to in Step 3.\n\n> **Info**\n>\n> `init()` starts delivery asynchronously, so the client is not connected the moment `init()` returns. Watch `delivery_state_changed` for the `online` state before creating conversations or sending messages.\n\n`init()` takes a single `ChatConfig` record, generated as the `ChatModule::ChatConfig` struct. Both fields are optional:\n\n| Field | Type | Notes |\n|---|---|---|\n| `delivery_preset` | string | The delivery network to join. Absent or empty means `logos.test`, the Logos test network. Must match across all participants. |\n| `log_level` | string | The chat core's own log verbosity: `error`, `warn`, `info`, `debug`, or `trace` (default `info`) |\n\n1. Initialise the chat client, at the end of `onContextReady()`:\n\n   ```cpp\n   ChatModule::ChatConfig config;\n   config.delivery_preset = QStringLiteral(\"logos.test\");\n   config.log_level = QStringLiteral(\"info\");\n   const LogosResult res = chat.init(config);\n   if (!res.success) {\n       qWarning() << \"init failed:\" << res.getError<QString>();\n       return;\n   }\n   // init succeeded; delivery connects asynchronously.\n   // Wait for delivery_state_changed with state == \"online\" before creating conversations.\n   ```\n\n   - `delivery_module` runs one node per process, shared by every module that uses it. If another module already created that node, `chat_module` uses it as it is, with that module's preset, and reports `deliveryAdopted == true`.\n\n1. Read (or set) your installation's display name. It is saved in the instance directory and not shown to peers:\n\n   ```cpp\n   const QString myName = chat.get_installation_name();\n   // Optionally choose a name: chat.set_installation_name(\"alice\");\n   ```\n\n1. Share your address:\n\n   ```cpp\n   const QString myAddress = chat.get_address();\n   // share `myAddress` out of band (the peer pastes it—see Steps 9 and 10)\n   ```\n\n   - The address is your account's address. Each `init()` creates a new account, so the address changes on every launch.\n\n## Step 5: Direct conversations\n\nA direct conversation is 1:1 - the initiator opens it with the peer's address, and messages flow both ways end-to-end encrypted.\n\n> **Info**\n>\n> To create a group conversation, skip this step and proceed to [Step 6](#step-6-group-conversations).\n\n1. Open the conversation as the initiator, or receive it as the recipient:\n\n   ```cpp\n   const LogosResult res = modules().chat_module.create_conversation(peerAddress);\n   const QString convoId = res.getValue<QString>();\n   // On success a conversation_created event (isOutgoing == true) fires with the same convoId.\n   ```\n\n   - The initiator calls `create_conversation` with the peer's address (their `get_address()`).\n   - The recipient does not call anything; a `conversation_created` push event (`isOutgoing == false`) arrives automatically once the invite lands.\n\n1. Send a message:\n\n   ```cpp\n   modules().chat_module.send_message(convoId, \"How are you?\");\n   // On success a message_sent event fires locally; the peer receives a message_received event.\n   ```\n\n   - Message content is plain text in both directions—the module handles encoding and end-to-end encryption on the wire.\n   - Success means the message was handed to `delivery_module`, not that a peer received it.\n\n1. Receive messages through the `message_received` event.\n\n## Step 6: Group conversations\n\nA group conversation starts with its creator as the only member and grows one peer at a time. Unlike a [direct conversation](#step-5-direct-conversations), membership changes are asynchronous by design: an add is committed by the group some time after it is proposed.\n\n1. Create the group:\n\n   ```cpp\n   const LogosResult res = modules().chat_module.create_group_conversation(\"Book Club\", \"Weekly sci-fi picks\");\n   const QString groupId = res.getValue<QString>();  // the conversation id every member will share\n   ```\n\n   - The group starts with you as its only member. The name and description are shared metadata, carried to every joiner. Note that this metadata is only set once at the moment of group creation and cannot be edited later.\n\n1. Grow the group one member at a time:\n\n   ```cpp\n   modules().chat_module.add_group_member(groupId, peerAddress);\n   // Returns once the add is *proposed*; the group commits it asynchronously.\n   ```\n\n   - `add_group_member` returns as soon as the add is proposed. The process takes a few minutes, then the peer will see the group chat on their side.\n   - The invited peer does not call anything; a `conversation_created` event (`kind == \"group\"`, carrying the group's shared name and description) arrives once the welcome lands.\n   - Membership is symmetric: **any** member can propose an add, not just the creator.\n   - A `members_changed` event fires on every membership change.\n   - Members can not be removed.\n\n1. Send a message to the group:\n\n   ```cpp\n   modules().chat_module.send_message(groupId, \"hello group\");\n   // One send reaches every member; each receiver gets a message_received event.\n   ```\n\n1. Receive group messages through the same `message_received` event.\n\n## Step 7: Read state and shut down\n\n1. Read history and conversation state at any time (synchronous reads):\n\n   ```cpp\n   auto& chat = modules().chat_module;\n   const QList<ChatModule::Conversation> convos = chat.list_conversations();\n   const QList<ChatModule::Message> msgs = chat.get_messages(convoId);\n   const ChatModule::Status st = chat.status();  // convo_count, delivery_state, detail, delivery_adopted\n   ```\n\n   - Optional record fields are `std::optional`—for example, `convo.name.value_or(QString())` or `msg.sender.value_or(QString())`.\n   - A conversation from an earlier session has `history_only == true`. You can read it, set its nickname, and delete it, but sending into it or inviting to it fails. A member inviting this installation back into such a group makes it live again.\n   - After a restart, `get_messages` returns a conversation's newest 500 messages.\n\n   > **Warning**\n   >\n   > Do not make a synchronous module read (`list_conversations`, `get_messages`, `status`) from *inside* an event handler—it re-enters the IPC replica while its read notifier is disabled and stalls until the call times out. Defer the read to the next event-loop turn instead (see `deferToEventLoop` in [`logos-chat-ui`](https://github.com/logos-co/logos-chat-ui/blob/v0.3.0/src/ChatBackend.cpp)).\n\n1. Read a group conversation roster at any time:\n\n   ```cpp\n   const QList<ChatModule::GroupMember> members = modules().chat_module.list_group_members(groupId);\n   // Each element: { address, pending }—committed members first, then invites\n   // this instance sent that the group has not committed yet (pending == true).\n   ```\n\n   - The `pending` flag clears when the group commits that add. An unknown conversation id reports an empty roster.\n\n1. Shut down cleanly:\n\n   ```cpp\n   modules().chat_module.shutdown();   // disconnects and tears the client down\n   ```\n\n   - `shutdown()` ends the installation. The next `init()` comes up with a new address, and this session's conversations read back as `history_only`.\n\n## Step 8: Build and run\n\n1. Build the module:\n\n   ```sh\n   nix build\n   ```\n\n1. Preview the module using `logos-standalone-app` (for `ui_qml` modules):\n\n   ```sh\n   nix run                # preview via logos-standalone-app (for ui_qml modules)\n   nix build .#lgx        # package as .lgx for installation into logos-basecamp\n   ```\n\n## Step 9: Verify a two-instance direct exchange\n\nA chat is only proven end to end when a message travels between two running instances. Start two copies of your module—each with its own instance directory—and confirm a message lands as a `message_received` event. Per-instance isolation comes from the host's instance directory (for example `nix run . -- --user-dir ~/.local/share/chat_a`).\n\n1. Start both instances and wait until each reports `delivery_state == \"online\"` via `delivery_state_changed`.\n1. **In instance A**: call `get_address()` and share the returned address out of band (copy it into instance B).\n1. **In instance B**: call `create_conversation(addressFromA)`.\n   - Instance B sees a `conversation_created` event (`isOutgoing == true`); instance A should receive one with `isOutgoing == false` once the invite lands.\n1. **In instance B**: send the first message: `send_message(convoId, \"Hello from B\")`.\n   - Instance A should receive a `message_received` event carrying `\"Hello from B\"`.\n1. **In instance A**: reply with `send_message(convoId, \"Hi B\")`.\n   - Instance B should receive the matching `message_received` event.\n\nSeeing the `message_received` events on both sides confirms the full round trip: identity, address exchange, key discovery, conversation setup, and end-to-end encrypted delivery.\n\n## Step 10: Verify a three-instance group conversation\n\nGroup semantics only show with three or more members. Start three instances (A, B, and C) by following the instructions in [Step 9](#step-9-verify-a-two-instance-direct-exchange) and wait for all three to report `online`. Group joins land asynchronously (see [Step 6](#step-6-group-conversations))—budget minutes per membership change.\n\n1. Collect each instance's address via `get_address()`.\n1. **In instance A**: call `create_group_conversation(\"Book Club\", \"Weekly sci-fi picks\")` and keep the returned conversation id.\n1. **In instance A**: call `add_group_member(groupId, addressOfB)`.\n   - The call returns immediately; B joins once the group commits the add—B receives a `conversation_created` event (`kind == \"group\"`, `name == \"Book Club\"`) under the same conversation id A holds.\n1. **In instance B**: Once B has joined, call `add_group_member(groupId, addressOfC)`.\n   - C can join the same way.\n1. In each instance, call `list_group_members(groupId)` and wait until all three addresses appear with `pending == false` on every member. Each membership change also fires `members_changed`.\n1. **In instance A**: call `send_message(groupId, \"hello group\")`.\n   - B and C each receive one `message_received` event whose `sender` is A's address.\n1. **In instance C**: post a reply.\n   - A and B should receive it, attributed to C's address—the newest member reaches the founding members and vice versa.\n\n## Troubleshooting the Logos Chat module\n\n### Why does a method fail?\n\nThe method returns a `LogosResult` with `success == false` and a reason in `getError<QString>()`. The most common cause is calling a conversation or message method before `init()` succeeded, or before delivery reached the `online` state. Call `init()` first, check `res.success`, and wait for a `delivery_state_changed` event with `delivery_state == \"online\"` before creating conversations or sending messages. Sending into or inviting to a `history_only` conversation also fails—see [Why can't I send into a conversation after a restart?](#why-cant-i-send-into-a-conversation-after-a-restart)\n\n### Why do peers not connect or messages not propagate?\n\nThe `delivery_preset` differs across instances, or delivery has not reached `online`. All participants must use the same preset (for example `logos.test`) to share a network, and each instance must report `online` via `delivery_state_changed` (or `status()`) before it can exchange messages. If `deliveryAdopted` is `true`, the node was created by another module and runs on that module's preset, not the one you passed to `init()`. All participants also need `chat_module` 0.3.0 or later: an instance on 0.2.x cannot reach one on 0.3.x. Each instance must also be able to reach the key-package registry at `https://devnet.chat-kc.logos.co` over HTTPS, which `init()` publishes to and `create_conversation` looks up. The registry is an API endpoint: opening its root URL in a browser returns `404 Not Found` even when it is working. For delivery-level detail, check the log file named by `get_log_path()`.\n\n### Why does `send_message` succeed but peers never receive the message?\n\n`chat_module` sends through `delivery_module`, which rate-limits with RLN (Rate-Limiting Nullifier) wherever the network preset enables it. `delivery_module` v0.3.0 and later enable RLN on `logos.test`, so sending there needs an active RLN membership; see [Register an RLN membership from Basecamp](https://docs.logos.co/lez/rln/register-rln-membership-from-basecamp.md). Without one, the node starts and receives, but delivery retries each send and then gives up.\n\n### Why hasn't an invited member joined the group yet?\n\nThis is the designed behaviour, not a hang. An `add_group_member` call only *proposes* the add: the group agrees on it, the group's steward batches it into an MLS commit after a commit-inactivity window (60 seconds by default), and the welcome only travels after that commit—so a join lands minutes after the call over the live network. Until the commit, the invite shows in the proposer's `list_group_members` with `pending == true`. Poll `list_conversations` on the invited instance (or wait for its `conversation_created` event) rather than assuming failure. An invite the group never commits stays pending for the life of the conversation.\n\nIn a group of two, once either member has restarted, a new add never completes: the commit needs both members, and the restarted member's old address never votes.\n\n### Why is `send_message` rejected right after a membership change?\n\nBriefly after a membership change commits, the group is finalising its new epoch and rejects sends. Retry after a few seconds.\n\n### Why does a read stall the UI?\n\nYou issued a synchronous module read (`list_conversations`, `get_messages`, `status`) from inside an event handler. That re-enters the IPC replica while its read notifier is disabled and blocks until the call times out. Defer such reads to the next event-loop turn.\n\n### Why can't I send into a conversation after a restart?\n\nEach launch is a new installation with a new address. Conversations from earlier sessions come back from the instance directory with `history_only == true`: you can read their messages, but not send into them or invite to them. To talk again, share your new address and open a new direct conversation, or have a group member invite your new address back into the group. Peers who still write to your old address are not told it changed. Restoring live conversations across restarts is planned for a later release.\n"},{"id":"storage/introduction-to-logos-storage","title":"Introduction to Logos Storage","summary":"Learn how Logos can be used to build resilient, censorship-resistant file sharing applications.","section":"storage","doc_type":"concept","url":"https://docs.logos.co/storage","markdown_url":"https://docs.logos.co/storage.md","permalink":"/storage","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/introduction-to-logos-storage.md","headings":[{"depth":2,"text":"The basics"},{"depth":2,"text":"How Logos Storage works"},{"depth":3,"text":"Organic replication"},{"depth":3,"text":"Privacy"},{"depth":2,"text":"How to use Logos Storage"}],"markdown":"# Logos Storage\n\n#### Learn how Logos can be used to build resilient, censorship-resistant file sharing applications.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Logos storage](https://docs.logos.co/get-started/glossary.md#logos-storage) is a filesharing protocol that allows users building on the Logos stack to publish and share files in a decentralised manner. It provides the persistence guarantees of an [organically replicated network](#organic-replication) while adding [stronger privacy guarantees](#privacy). Logos storage [is provided as a Logos module](https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md), and requires the Logos runtime and tooling to work.\n\n## The basics\n\n- Logos Storage allows users to share files over a decentralised, private network of nodes.\n- Files are stored locally by each node, with each file assigned a unique identifier known as a [CID](https://docs.logos.co/get-started/glossary.md#cid).\n- Anybody with a CID can download the associated file from the network. Nodes that download a file replicate it on their machine and make it available for others, with more downloads resulting in more peers to download from.\n- Neither providers nor downloaders can be linked to the content they interact with.\n\n## How Logos Storage works\n\nThe key functionality of Logos Storage is _file sharing_: you can make files available on the network which other people can then download. The basic process is illustrated in Figure 1:\n\n![Logos storage workflow.](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/logos-intro.png)\n\n1. A user [uploads a file to their Logos Storage node `p`](https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md#publish-a-file).\n1. Node `p` stores the file internally.\n1. Node `p` produces a *Content Identifier* (CID for short) that uniquely identifies the file. The CID functions very much like a \"file share link.\"\n1. The user shares the CID with other users via a third-party communication mechanism (such as [Logos Messaging](https://docs.logos.co/messaging.md)).\n1. Another user that received the CID uses their own Logos Storage node `q` to download the file.\n1. Node `q` that downloaded the file stores it internally.\n1. Once the file is made available on node `q` (including partial availability, such as for streaming), the user can retrieve it and consume its contents.\n\n### Organic replication\n\nNote that a user that wants to share a file must first upload it to their local node. Another user that wants to obtain that same file must then download it to their own local node first. The reason is that, underneath, both users are engaging in a replication protocol - when the first user initially shares their file, that file is added to the network with 1 replica (the uploading node). Once the second user downloads it, the file has 2 replicas. If a third user were to download that same file as well, it would have 3 replicas, etc.\n\nSince the number of replicas is determined by the number of nodes interested in a given file - interest being an organic property - this process is referred to as _organic replication_. This is in stark contrast to decentralised storage networks such as [Swarm](https://www.ethswarm.org/The-Book-of-Swarm.pdf) or [SIA](https://sia.tech/), where providers guarantee the availability of uploaded content in exchange for a fee. The way Logos Storage works places a fundamental limitation on persistence: if no one is interested in your files, chances are that losing your node means your data is lost as well.\n\nThis is not an insurmountable limitation: one could set up multiple nodes and create replication groups for files using Logos Messaging, but this is not currently provided out-of-the-box. For files of public interest, however, replication should happen naturally: parties interested in keeping some content online can operate their own nodes and curate the list of files they wish to replicate. This allows the operator to control the type of content they host, as well as how much content they host.\n\n### Privacy\n\nA key problem with filesharing networks such as [BitTorrent](https://www.bittorrent.org/beps/bep_0003.html) - and Logos Storage itself - is that organic replication on its own exposes both the party that provides content (the *provider*) and the party that downloads it (the *downloader*) to censorship. Even with encrypted communications, active attackers can join the network and uncover which nodes are doing what.\n\nPrivacy in Logos Storage has been focused on protecting the privacy of both providers and downloaders by the use of [Mix networks](https://doi.org/10.1145/358549.358563). This is strongly inspired by how [Tribler](https://tribler.org/) works, but adapted to Logos' infrastructure and constraints. When fully mature, Logos Storage will leverage mix network technology to provide _privacy-preserving filesharing_; that is, a system hat aims to protect providers’ and downloaders’ privacy by making it difficult to link them to the content they provide or download.\n\n## How to use Logos Storage\n\nThe [Storage App](https://docs.logos.co/storage/get-started/run-storage-node-from-basecamp.md) is probably the fastest and simplest way to try out Logos Storage. To run a node on a more permanent basis or build on Logos Storage, check out [how to run a storage node](https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md) and the [Storage module API reference](https://logos-co.github.io/logos-storage-module/latest/api_reference.html).\n"},{"id":"storage/get-started/run-storage-node-from-basecamp","title":"Run a Storage Node from Basecamp","summary":"Get started sharing and downloading files on the Logos Storage network","section":"storage","doc_type":"procedure","url":"https://docs.logos.co/storage/get-started/run-storage-node-from-basecamp","markdown_url":"https://docs.logos.co/storage/get-started/run-storage-node-from-basecamp.md","permalink":"/storage/get-started/run-storage-node-from-basecamp","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/get-started/run-storage-node-from-basecamp.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Run the application"},{"depth":2,"text":"Step 2: Configure your node through onboarding"},{"depth":3,"text":"Configuration"},{"depth":2,"text":"Step 3: Share a file"},{"depth":2,"text":"Step 4: Download a file"},{"depth":2,"text":"Step 5: Fetch and download privately with Mix"},{"depth":2,"text":"Step 6: Settings"},{"depth":2,"text":"Step 7: Debug"},{"depth":2,"text":"Step 8: Manage the node lifecycle"},{"depth":2,"text":"Troubleshooting Logos Storage"}],"markdown":"# Run a Storage Node from Basecamp\n\n#### Get started sharing and downloading files on the Logos Storage network\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThe [Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) app is a file-sharing application built on top of the [Logos Storage Module](https://github.com/logos-co/logos-storage-module). This guide covers running the application through Logos [Basecamp](https://docs.logos.co/get-started/glossary.md#basecamp), configuring your node through the onboarding wizard, and using the app to share, download, and delete files. It is intended for node operators running the application on Linux, macOS, or Windows through Basecamp.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux\n>     - Mac OS (should work, but not tested)\n>     - Windows 10 or 11 x86_64. The commands in this guide use POSIX shell syntax, so run them in Git Bash (part of [Git for Windows](https://git-scm.com/download/win)).\n> - [Logos Basecamp](https://docs.logos.co/basecamp/install-logos-basecamp.md) version [0.3.2](https://github.com/logos-co/logos-basecamp/releases/tag/0.3.2) or later, installed and running.\n\n## What to expect\n\n- You can run the Logos Storage app on Basecamp.\n- You can configure your node through the onboarding wizard, in guided or advanced mode, and reach a running node.\n- You can share files with other nodes and download files shared by others using a Content Identifier ([CID](https://docs.logos.co/get-started/glossary.md#cid)).\n- You can fetch a manifest and download its content privately over Mix, and stop and restart the node without losing your files.\n\n## Step 1: Run the application\n\n1. In the left bar of Basecamp, select **Package Manager**.\n1. Select `Storage` in `Categories`, then click **INSTALL** in the **Storage** row.\n1. In the **Install Package?** dialogue, which lists the `storage_module` [module](https://docs.logos.co/get-started/glossary.md#module) as a dependency to install alongside the UI, click **Install**.\n1. Wait until the **Action** column of the **Storage** row reads **INSTALLED**. A new **Storage** icon appears in the left bar.\n1. In the left bar, select the **Storage** icon to launch the Logos Storage app.\n\n## Step 2: Configure your node through onboarding\n\nOn first launch, the [app](https://docs.logos.co/get-started/glossary.md#app) opens the onboarding wizard and asks how you want to set up your node: **Guided** or **Advanced**.\n\nThe Guided option will use the default configuration and start the node automatically.\nThe Advanced option allows you to edit the configuration before starting the node.\n\nThis guide follows the `Guided` setup.\n\n1. Select **Guided** and click **Continue**.\n\n   ![Onboarding](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-onboarding.png)\n\n1. On the **Select Drives** step, choose the folder where downloaded files will be saved, then click **Continue**.\n\n   ![Select Drives step with the downloads folder field](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-select-drives.png)\n\n1. Wait for the dashboard to open and the node status icon to be green.\n\n   ![Storage app dashboard with the node running](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-dashboard.png)\n\n   A red status dot means the node is not running.\n\n   When you start the node, you will see a status `Unknown` with a grey dot blinking. This is the NAT status check, updated every minute.\n   A grey dot means the reachability check has not been performed yet.\n   A green dot means the node is reachable from the network and can upload and download content.\n   An orange dot means the node is not reachable from the network but falls back to the relay network. The node can still upload and download content using a relay with other peers. The speed might be reduced unless both peers were able to create a direct connection (hole punching).\n\n   See [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md).\n\n1. Clicking on the help icon on the right of the NAT status will give you more information about the node's reachability.\n\n   ![NAT status](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-nat.png)\n\n### Configuration\n\nAfter onboarding, the settings are saved to a file whose location depends on the OS:\n\n| OS    | Path                                                |\n|:------|:-----------------------------------------------------|\n| Linux | `~/.config/Logos/LogosBasecamp.conf`                  |\n| macOS | `~/Library/Preferences/com.logos.LogosBasecamp.plist` |\n\n## Step 3: Share a file\n\n1. In the **Upload** panel, click **browse**. A file selector opens.\n\n1. Select the file you want to share and click **Open**. The file is uploaded to your node and sharing begins automatically.\n\n1. When the upload completes, the file appears in the **Manifests** list at the bottom of the UI, with its CID, filename, mimetype, and size.\n\n   ![Dashboard after an upload, with the file listed in the Manifests panel](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-uploaded.png)\n\n1. Click the copy icon next to the CID. Share this string with others so they can download the file.\n\n## Step 4: Download a file\n\nThe manifest is the representation of a file on the network: it carries the metadata (filename, size, mimetype). To download a file, you first fetch its manifest by CID, then download the content itself.\n\n1. Paste the file's CID into the **Fetch Manifest** panel and click **Fetch**. The manifest downloads from the network and an entry appears in the **Manifests** list.\n\n1. In the manifest entry's **Actions** column, click the download icon.\n\n1. Watch the download widget at the top: it shows progress in real time and reports **Complete** when the file has been written to the downloads folder you chose during onboarding.\n\n   ![Dashboard after a download, with the download widget reporting Complete](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-downloaded.png)\n\n> **Info**\n>\n> No CID at hand? Try downloading a public file: fetch `zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ`, the [Farewell to Westphalia book](https://logos.co/farewell-to-westphalia). It is available on the network the default configuration connects to.\n\nWhen the file is downloaded, the download icon turns orange. This means the file is in your downloads folder.\n\n## Step 5: Fetch and download privately with Mix\n\nA private fetch or download goes over the Logos mix network. This makes it much harder to trace back to you. See [Mix](https://docs.logos.co/storage/concepts/mix.md) for how it works.\n\n1. To fetch a manifest privately, turn on the `Private` switch in the `Fetch Manifest` panel, then click `Fetch`.\n\n1. To download a file privately, click the lock icon in the manifest entry's `Actions` column. The icon turns orange. A private download is not advertised to the network.\n\n   ![Private switch and lock icon turned on](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-private.png)\n\n- By default, fetches and downloads are not private.\n- After a private fetch, the lock icon is already on. Click it to download directly.\n- Private transfers are slower.\n\n> **Warning**\n>\n>\n> When using `nat:auto`, the node first needs to get a reachability status, `Reachable` or `Unreachable`, before it can make DHT queries. See [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md) for details.\n>\n\n## Step 6: Settings\n\nIn the top right section, the settings icon on the right of **Manage node** opens the settings popup.\n\n![Settings 1](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-settings-1.png)\n\n![Settings 2](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-settings-2.png)\n\nThe default configuration should be suitable for most users.\n\nSome settings cannot be updated, such as the `Data directory`.\nMost of the editable settings will require a node restart to take effect. You should see a message indicating that on the bottom right:\n\n> `Unsaved changes — the node must restart to apply them.`\n\nAfter you click `Save`, the message changes to:\n\n> `Saved. Restart the node to apply and save to disk.`\n\nThe new configuration is saved to disk only when the node restarts. Close the settings and click `Stop` then `Start` to restart the node.\n\n> **Info**\n>\n> The active configuration is saved to `${HOME}/.logos_storage/config.json`. This file should not be edited manually. The settings UI should be used to change the configuration instead. If your configuration is messed up, you can delete this file and restart the node to reset it to the default configuration.\n\n## Step 7: Debug\n\nA debug popup is available on the top right section, next to the settings icon. You can also display it using the `Ctrl+D` keyboard shortcut.\n\n![Debug](https://raw.githubusercontent.com/logos-co/logos-docs/main/docs/storage/assets/set-up-and-use-logos-storage-ui/storage-ui-debug.png)\n\nWhile it seems to be oriented toward advanced users, it can be useful to check the node's reachability status and if the relay is running.\n\n## Step 8: Manage the node lifecycle\n\n1. To stop the node, click **Stop** in the **Node** panel. The status indicator turns red, the node reports **Stopped**, and peer connections drop.\n\n1. Click **Start** to start the node again. The status goes back to the reachability check (**Unknown**, then **Reachable** or **Not reachable**) and peers reconnect.\n\n   - Your files survive the restart: the node persists its data in the configured `data-dir`, so previously uploaded files reappear in the **Manifests** list.\n\n1. To stop sharing a file, click the trash icon in the manifest entry's **Actions** column. The file leaves the list and the **Storage** panel returns to **0 B Utilised**: the blocks are actually removed from disk.\n\n   - `.lgx` files have no trash icon: they are module [packages](https://docs.logos.co/get-started/glossary.md#package) downloaded from the Logos Package Manager in Basecamp.\n\n## Troubleshooting Logos Storage\n\nConnectivity problems (no peers, unreachable node, downloads timing out) are covered in the [Troubleshooting](https://docs.logos.co/storage/get-started/faq.md) and [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md) pages.\n"},{"id":"storage/get-started/run-storage-node-from-cli","title":"Run a Storage node from the CLI","summary":"Get started running a Logos Storage node and uploading your first file to the Logos network.","section":"storage","doc_type":"procedure","url":"https://docs.logos.co/storage/get-started/run-storage-node-from-cli","markdown_url":"https://docs.logos.co/storage/get-started/run-storage-node-from-cli.md","permalink":"/storage/get-started/run-storage-node-from-cli","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/get-started/run-storage-node-from-cli.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Load the Logos Storage module"},{"depth":2,"text":"Configure and start the node"},{"depth":2,"text":"Publish a file"},{"depth":2,"text":"Download Farewell to Westphalia"},{"depth":2,"text":"Remove content and shut everything down"},{"depth":2,"text":"Troubleshooting Logos Storage"}],"markdown":"# Run a Storage node from the CLI\n\n#### Get started running a Logos Storage node and uploading your first file to the Logos network.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure covers how to build and run the [Logos Storage Module](https://github.com/logos-co/logos-storage-module/), connect it to the testnet bootstrap nodes, publish a file, and verify that the file can be downloaded. It is intended for node operators on testnet v0.3 who want to contribute storage capacity to the Logos network.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux\n>     - Mac OS (should work, but not tested)\n>     - Windows 10 or 11 x86_64. The commands in this guide use POSIX shell syntax, so run them in Git Bash (part of [Git for Windows](https://git-scm.com/download/win)).\n> - `jq` on your `PATH`.\n>     - To verify, run: `jq --version`\n>     - On Windows, install it with `winget install jqlang.jq`.\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n>    - On Windows, run `irm https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.ps1 | iex` in PowerShell instead, then open a new terminal.\n\n## What to expect\n\nIn this tutorial, you will:\n\n- Connect a [Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) node to the testnet.\n- Publish a file to the network.\n- Download an existing file - the Logos book, [Farewell to Westphalia](https://logos.co/book) - from the Logos storage network.\n\n## Load the Logos Storage module\n\n`logosctl` ships the Logos Storage [module](https://docs.logos.co/get-started/glossary.md#module) and loads it when the daemon starts, because the package downloader uses it as an optional dependency to fetch [packages](https://docs.logos.co/get-started/glossary.md#package). The package downloader may also have started the Storage node already, with the default configuration or the one saved by your last `init`. Stop it first so that you can start it with your own configuration.\n\n1.  Start `logosctl`:\n\n    ```sh\n    logosctl daemon start\n    ```\n\n1.  In a new terminal window with the same user, confirm that `storage_module` is loaded:\n\n    ```sh\n    logosctl module ls\n    ```\n\n    - If it isn't listed, install it from the [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue), pinned to version 3.0.0, and load it:\n\n    ```sh\n    logosctl catalog refresh\n    logosctl package install storage_module --version 3.0.0 --yes\n    logosctl module load storage_module\n    ```\n\n    - A `module load` sent before the daemon is ready fails with an RPC or missing client config error. If that happens, check `logosctl status` again and retry.\n\n    > **Note**\n    >\n    > Individual module package versions (for example, Storage module version 3.0.0) are pinned independently and do not necessarily match the testnet version number (0.3.0).\n\n1.  Check whether the Storage node is already running:\n\n    ```sh\n    logosctl call storage_module isRunning\n    ```\n\n1.  If the result is `true`, stop the node, wait until `isRunning` returns `false`, then destroy it:\n\n    ```sh\n    logosctl call storage_module stop\n    # Wait a few seconds, then check that isRunning returns false\n    logosctl call storage_module isRunning\n    logosctl call storage_module destroy\n    ```\n\n    - Don't unload and reload the module instead: the package downloader starts the node again as soon as the module is back.\n\n## Configure and start the node\n\nInitialise and start the Storage module with `logosctl`.\n\nSeveral module calls in this procedure are **asynchronous**: the call returns `\"result\":true` as soon as the command is accepted, and the real outcome is delivered later as an event (`storageStart`, `storageUploadDone`, `storageDownloadDone`, `storageRemoveDone`, `storageDownloadManifestDone`). These events are emitted to event subscribers (such as the Storage UI); the `logosctl call` client does not subscribe to them, so they do **not** appear in the daemon log (`~/.logosctl/logs/daemon.log`). Each step below instead waits briefly and confirms the outcome with a follow-up query (for example `manifests` or `exists`).\n\n> **Tip**\n>\n> To see every method the module exposes (the same methods you can `call`), run `logosctl module-info storage_module`.\n\n1.  Use the Storage module to produce a suitable default configuration:\n\n    ```sh\n    logosctl call storage_module loadConfigOrDefault | jq \".result.value | fromjson\" > config.json\n    ```\n\n    Now edit the configuration and modify:\n\n      * `data-dir`: should point to an absolute path on your disk, to which you have write access. On Windows, use a Windows path such as `C:/Users/<you>/logos-storage-data`: Git Bash converts paths in command arguments, but not inside the JSON file;\n      * `listen-port`: should contain a valid TCP port which is currently free on your local machine.\n\n    > **Tip**\n    >\n    > - The default settings for Logos Storage should be enough to get your node properly connected onto the Logos testnet. In case you want more control over port allocation, or want to learn more about how Logos storage operates, see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md).\n    > - If you plan on running a node for longer, consider helping the network by setting up port mapping on your router (see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md) for details).\n\n1.  Initialise the Storage module. `init` is synchronous and returns `true` on success (the `@config.json` syntax loads the file's contents as the argument):\n\n    ```sh\n    logosctl call storage_module init @config.json\n    ```\n\n1.  Start the node. `start` is asynchronous: the return value only confirms the command was accepted; completion is signalled later by the `storageStart` event (delivered to event subscribers, not written to the daemon log):\n\n    ```sh\n    logosctl call storage_module start\n    # Wait few seconds to start\n    ```\n\n1.  Inspect the running node with `debug`. It returns a lot of information about the node, including its `id` ([peer ID](https://docs.logos.co/get-started/glossary.md#peer-id)) and its `spr`, the signed record other nodes use to connect to you (see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md)):\n\n    ```sh\n    logosctl call storage_module debug | jq .result.value.id\n    # \"16Uiu2HAmMA4NuQoCHz9p7jUskVjDd8WncwG3p6qBNnhnftUE5Q9C\" # Your peer ID\n    logosctl call storage_module debug | jq .result.value.spr\n    # \"spr:CiUIAhIhA35P5KZosVyfWTfIHBVtC_PtI ... H9gX-vA\" # Your SPR\n    ```\n\n## Publish a file\n\nWe will now publish a file to the Logos Storage network. We create a simple file just for this tutorial, but you could publish any file you'd like.\n\n1.  Create a file to publish:\n\n    ```sh\n    echo \"Hello world from Logos Storage\" > \"$(pwd)/hello.txt\"\n    ```\n\n1.  Upload the file to the network with `uploadUrl`. It takes an **absolute** path, a chunk size in bytes, and an `advertise` flag—`true` announces the file and serves it to other nodes. It returns immediately; the upload runs in the background and completes with a `storageUploadDone` event:\n\n    ```sh\n    logosctl call storage_module uploadUrl \"$(pwd)/hello.txt\" 65536 true\n    ```\n\n    > **Info**\n    >\n    > The default chunk size is 65536.\n\n1.  Extract the content ID ([CID](https://docs.logos.co/get-started/glossary.md#cid)) from the first `manifests` entry:\n\n    ```sh\n    # Wait a second for the upload to complete first\n    logosctl call storage_module manifests \\\n       | jq -er '.result.value[0].cid' > cid.txt\n    ```\n\n## Download Farewell to Westphalia\n\nWe will now download the Logos book, [Farewell to Westphalia](https://logos.co/book), from the Logos Storage network.\n\n1. We will use `downloadToUrl` to download the file from the network and place it into your local disk. It takes:\n    * the CID;\n    * an **absolute** destination path;\n    * a `local` flag;\n    * a chunk size in bytes;\n    * an `isPrivate` flag, and;\n    * an `advertise` flag.\n\n   We set `local` to `false` as this file is not currently available in your node; `isPrivate` to false so we download directly\n   from a storage provider instead of using the [Logos mix network](https://docs.logos.co/storage/concepts/mix.md), and `advertise` to false so we do not\n   advertise ourselves as providers to the file.\n\n   Like `uploadUrl` it runs in the background and completes with a `storageDownloadDone` event:\n\n    ```sh\n    CID=\"zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\"\n    logosctl call storage_module downloadToUrl \"$CID\" \"$(pwd)/farewell-to-westphalia.pdf\" false 65536 false false\n    ```\n\n    > **Note**\n    >\n    > - Setting `advertise` to `true` when you run an ephemeral node (a node that runs briefly and is then shut down) can be detrimental to network performance as your node announces itself and then leaves an advertisement pointing to a departed node.\n    > - When you download over mix (not shown here), you typically also do not want to set `advertise` to `true`, as that would reveal to other nodes that you've downloaded the file, defeating the purpose of the using mix in the first place.\n    > - The `local` flag reads only from locally cached data when set to `true`; `false` fetches from the network.\n\n1.  Wait for a while for the file to download. After a few seconds, check if the downloaded file is present at the destination path. You should try to open the pdf, and it should contain the whole book.\n\n    ```sh\n    shasum \"$(pwd)/farewell-to-westphalia.pdf\"\n    # 2c6b4dc8e8e4dae336b87b9922c38f3c94217872  farewell-to-westphalia.pdf\n    ```\n\n## Remove content and shut everything down\n\nTo clear your local storage, destroy the Storage node, and stop the daemon, follow the steps below.\n\n1.  Remove content from local storage by its CID. `remove` returns immediately; the outcome arrives as a `storageRemoveDone` event:\n\n    ```sh\n    # Deletes the first file we uploaded.\n    logosctl call storage_module remove \"$(cat cid.txt)\"\n    # Deletes the Farewell to Westphalia book.\n    logosctl call storage_module remove \"zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\"\n    ```\n\n1.  Confirm the content is gone:\n\n    ```sh\n    # Wait a second for the removal to complete first\n    logosctl call storage_module exists \"$(cat cid.txt)\" | jq '.result.value'\n    logosctl call storage_module exists \"zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\" | jq '.result.value'\n    # false\n    ```\n\n1.  Stop the Storage node. `stop` is asynchronous like `start`; completion is signalled by a `storageStop` event (delivered to event subscribers, not written to the daemon log). The node can be started and stopped multiple times:\n\n    ```sh\n    logosctl call storage_module stop\n    # Wait a few seconds for the node to stop before destroying it\n    ```\n\n1.  Destroy the storage context. `destroy` is synchronous and must be called after the node is stopped:\n\n    ```sh\n    logosctl call storage_module destroy\n    ```\n\n1.  Stop the daemon:\n\n    ```sh\n    logosctl daemon stop\n    ```\n\n## Troubleshooting Logos Storage\n\nConnectivity problems (downloads timing out from another machine, no peers, unreachable node) are covered in the [Troubleshooting](https://docs.logos.co/storage/get-started/faq.md) and [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md) pages.\n"},{"id":"storage/get-started/run-mix-network-of-storage-nodes","title":"Run a Mix network of Storage nodes","summary":"Stand up a local Mix network and download a file through it, with the content lookup routed through Mix for privacy.","section":"storage","doc_type":"procedure","url":"https://docs.logos.co/storage/get-started/run-mix-network-of-storage-nodes","markdown_url":"https://docs.logos.co/storage/get-started/run-mix-network-of-storage-nodes.md","permalink":"/storage/get-started/run-mix-network-of-storage-nodes","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/get-started/run-mix-network-of-storage-nodes.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Download and install the storage module"},{"depth":2,"text":"Launch the bootstrap Mix node (node 1)"},{"depth":2,"text":"Launch the remaining Mix relays (nodes 2–4)"},{"depth":2,"text":"Build the Mix relay pool"},{"depth":2,"text":"Start the storage nodes (5 and 6)"},{"depth":2,"text":"Upload from one node, download through Mix"},{"depth":2,"text":"Shut the network down"}],"markdown":"# Run a Mix network of Storage nodes\n\n#### Stand up a local Mix network and download a file through it, with the content lookup routed through Mix for privacy.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\nThis procedure stands up a small local [Mix](https://docs.logos.co/storage/concepts/mix.md) network using `logosctl`: six [Logos Storage Module](https://github.com/logos-co/logos-storage-module/) nodes on one machine—four Mix relays wired around a bootstrap node, plus two storage nodes that route their DHT lookups through the relays. At the end, one storage node uploads a file and the other downloads it with the lookup tunnelled over Mix.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux\n>     - Mac OS (should work, but not tested)\n> - `jq` on your `PATH`.\n>     - To verify, run: `jq --version`\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n\n## What to expect\n\n- You can run several `logosctl` daemons side by side, each in its own session (`--config-dir`).\n- You can set up a private Mix network and configure storage nodes to route their lookups through it for privacy.\n- You can exchange a file between two storage nodes and verify the content lookup was tunnelled over Mix.\n\n## Download and install the storage module\n\nAll six nodes below share one already-unpacked copy of `storage_module`, installed once into a throwaway session. Each node's session is then configured to also scan that directory (the `modules_dirs` key of its daemon configuration), so there's no need to repeat the install per node.\n\n1.  Start a throwaway session and install the storage module [package](https://docs.logos.co/get-started/glossary.md#package) into it. Package installs are handled by a module bundled inside the daemon, so the daemon has to be running first:\n\n    ```sh\n    logosctl daemon start --detach --config-dir ./install-session\n    logosctl --config-dir ./install-session catalog refresh\n    logosctl --config-dir ./install-session package install storage_module --version 3.0.0 --yes\n    ```\n\n1.  Confirm the module landed, then stop this session—its only job was the install:\n\n    ```sh\n    logosctl --config-dir ./install-session package ls\n    logosctl --config-dir ./install-session daemon stop\n    ```\n\n    The package is now unpacked under `./install-session/modules/storage_module/`.\n\n## Launch the bootstrap Mix node (node 1)\n\nThe first node is the bootstrap node: the other nodes use it to join the Mix network.\n\n1.  Write node 1's configuration. It uses `\"mix-enabled\": true` and `\"no-bootstrap-node\": true` because it is the bootstrap node. Paths are absolute (`$(pwd)`) because each module runs in its own process:\n\n    ```sh\n    mkdir -p \"$(pwd)/storage-data/node-1\"\n    cat > config-1.json <<EOF\n    {\n      \"log-level\": \"DEBUG\",\n      \"data-dir\": \"$(pwd)/storage-data/node-1\",\n      \"log-file\": \"$(pwd)/storage-data/node-1/storage.log\",\n      \"nat\": \"extip:127.0.0.1\",\n      \"listen-port\": 8081,\n      \"mix-enabled\": true,\n      \"no-bootstrap-node\": true\n    }\n    EOF\n    ```\n\n1.  Point node 1's session at the shared module directory, then start a `logosctl` daemon for it, detached. `logosctl` has no `--modules-dir` flag: extra module directories are set with `modules_dirs` in the session's daemon configuration, which `daemon config set -` reads from standard input. The daemon's logs go to `./logosctl-1/logs/daemon.log`, so there's no need to redirect output by hand:\n\n    ```sh\n    printf 'modules_dirs:\\n  - %s\\n' \"$(pwd)/install-session/modules\" \\\n      | logosctl --config-dir ./logosctl-1 daemon config set -\n    logosctl daemon start --detach --config-dir ./logosctl-1\n    ```\n\n1.  Load the module, initialise it, and start the node:\n\n    ```sh\n    logosctl --config-dir ./logosctl-1 module load storage_module\n    logosctl --config-dir ./logosctl-1 call storage_module init @config-1.json\n    logosctl --config-dir ./logosctl-1 call storage_module start\n    # Wait a few seconds for the node to start\n    ```\n\n1.  Read node 1's SPR out of `debug` and save it: the other nodes use this value as their `bootstrap-node` (see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md)):\n\n    ```sh\n    logosctl --config-dir ./logosctl-1 call storage_module debug \\\n      | jq -er '.result.value.spr' > bootstrap-spr.txt\n    ```\n\n## Launch the remaining Mix relays (nodes 2–4)\n\nNodes 2, 3 and 4 are identical to node 1, except that they join through node 1's SPR as their `bootstrap-node`.\n\n1.  Write the configs for nodes 2–4, with per-node ports and data dirs:\n\n    ```sh\n    BOOTSTRAP=$(cat bootstrap-spr.txt)\n    for id in 2 3 4; do\n      mkdir -p \"$(pwd)/storage-data/node-$id\"\n      cat > \"config-$id.json\" <<EOF\n    {\n      \"log-level\": \"DEBUG\",\n      \"data-dir\": \"$(pwd)/storage-data/node-$id\",\n      \"log-file\": \"$(pwd)/storage-data/node-$id/storage.log\",\n      \"nat\": \"extip:127.0.0.1\",\n      \"listen-port\": $((8080 + id)),\n      \"mix-enabled\": true,\n      \"bootstrap-node\": [\"$BOOTSTRAP\"]\n    }\n    EOF\n    done\n    ```\n\n1.  Point each node's session at the shared module directory and start one daemon per node, each detached:\n\n    ```sh\n    for id in 2 3 4; do\n      printf 'modules_dirs:\\n  - %s\\n' \"$(pwd)/install-session/modules\" \\\n        | logosctl --config-dir ./logosctl-$id daemon config set -\n      logosctl daemon start --detach --config-dir ./logosctl-$id\n    done\n    ```\n\n1.  Load the module, initialise from each config, and start each node:\n\n    ```sh\n    for id in 2 3 4; do\n      logosctl --config-dir ./logosctl-$id module load storage_module\n      logosctl --config-dir ./logosctl-$id call storage_module init @config-$id.json\n      logosctl --config-dir ./logosctl-$id call storage_module start\n    done\n    # Wait a few seconds for the nodes to start\n    ```\n\n1.  Verify the network is up: every node must report a non-empty identity (`id` and `spr`) through `debug`:\n\n    ```sh\n    for id in 1 2 3 4; do\n      started_up=$(logosctl --config-dir ./logosctl-$id call storage_module debug \\\n        | jq -e '(.result.value.id // \"\") != \"\" and (.result.value.spr // \"\") != \"\"')\n      if [ \"$started_up\" = \"true\" ]; then\n        echo \"Node $id is up\"\n      else\n        echo \"Node $id is down\"\n      fi\n    done\n    # You should see \"Node X is up\" for all nodes from 1 to 4\n    ```\n\n## Build the Mix relay pool\n\nThe storage nodes need two files to use the Mix relays:\n\n- `mix-pool.json`: the relays' [peer IDs](https://docs.logos.co/get-started/glossary.md#peer-id), multiaddrs, mix and libp2p public keys.\n- `mix-proxies.json`: the relays' TCP SPRs.\n\nSince this is a local network, every relay is reachable at `127.0.0.1` on its fixed `listen-port` (`808<id>`), so the pool `multiAddr` is built from that rather than from the announced addresses.\n\n1.  Save the debug output of each relay to `debug-<id>.json` to make the data extraction easier:\n\n    ```sh\n    for id in 1 2 3 4; do\n      logosctl --config-dir ./logosctl-$id call storage_module debug > debug-$id.json\n    done\n    ```\n\n1.  Assemble `mix-pool.json`. `debug` already returns `peerId`, `mixPubKey` and `libp2pPubKey` in exactly the form the pool wants:\n\n    ```sh\n    for id in 1 2 3 4; do\n      ADDR=\"/ip4/127.0.0.1/tcp/$((8080 + id))\"\n      jq --arg ma \"$ADDR\" '{\n          peerId: .result.value.id,\n          multiAddr: $ma,\n          mixPubKey: .result.value.mixPubKey,\n          libp2pPubKey: .result.value.libp2pPubKey\n        }' debug-$id.json\n    done | jq -s '{version: 1, relays: .}' > mix-pool.json\n    ```\n\n1.  Collect the relays' proxy SPRs (`spr`) into a JSON array:\n\n    ```sh\n    jq -s -c '[.[].result.value.spr]' debug-*.json > mix-proxies.json\n    ```\n\n## Start the storage nodes (5 and 6)\n\nThe four nodes so far are the Mix relays. Now add the storage nodes that actually use them: their config is the same, plus the `dht-mix-proxy` list and the `mix-pool` path built in the previous section.\n\n1.  Write the storage node configs:\n\n    ```sh\n    BOOTSTRAP=$(cat bootstrap-spr.txt)\n    PROXIES=$(cat mix-proxies.json)\n    for id in 5 6; do\n      mkdir -p \"$(pwd)/storage-data/node-$id\"\n      cat > \"config-$id.json\" <<EOF\n    {\n      \"log-level\": \"DEBUG\",\n      \"data-dir\": \"$(pwd)/storage-data/node-$id\",\n      \"log-file\": \"$(pwd)/storage-data/node-$id/storage.log\",\n      \"listen-port\": $((8080 + id)),\n      \"nat\": \"extip:127.0.0.1\",\n      \"mix-enabled\": true,\n      \"bootstrap-node\": [\"$BOOTSTRAP\"],\n      \"dht-mix-proxy\": $PROXIES,\n      \"mix-pool\": \"$(pwd)/mix-pool.json\"\n    }\n    EOF\n    done\n    ```\n\n1.  Point each storage node's session at the shared module directory and start one daemon per node, each detached:\n\n    ```sh\n    for id in 5 6; do\n      printf 'modules_dirs:\\n  - %s\\n' \"$(pwd)/install-session/modules\" \\\n        | logosctl --config-dir ./logosctl-$id daemon config set -\n      logosctl daemon start --detach --config-dir ./logosctl-$id\n    done\n    ```\n\n1.  Load the module, initialise from each config, and start each node:\n\n    ```sh\n    for id in 5 6; do\n      logosctl --config-dir ./logosctl-$id module load storage_module\n      logosctl --config-dir ./logosctl-$id call storage_module init @config-$id.json\n      logosctl --config-dir ./logosctl-$id call storage_module start\n    done\n    # Wait a few seconds for the nodes to start\n    ```\n\n1.  Verify the storage nodes are up:\n\n    ```sh\n    for id in 5 6; do\n      logosctl --config-dir ./logosctl-$id call storage_module debug \\\n        | jq -e '(.result.value.id // \"\") != \"\" and (.result.value.spr // \"\") != \"\"'\n    done\n    ```\n\n## Upload from one node, download through Mix\n\nNode 5 seeds a file, and node 6 downloads it with `local=false` to force a network lookup—the lookup that Mix hides.\n\n1.  Create a small file and upload it through node 5:\n\n    ```sh\n    echo \"Hello through Mix from the storage doc-test.\" > hello.txt\n    logosctl --config-dir ./logosctl-5 call storage_module uploadUrl \"$(pwd)/hello.txt\" 65536 true\n    ```\n\n1.  The upload runs in the background; give it a moment, then read the [CID](https://docs.logos.co/get-started/glossary.md#cid) of the stored manifest from node 5:\n\n    ```sh\n    logosctl --config-dir ./logosctl-5 call storage_module manifests \\\n      | jq -er '.result.value[0].cid' > cid.txt\n    ```\n\n1.  Download the CID through node 6:\n\n    ```sh\n    logosctl --config-dir ./logosctl-6 call storage_module downloadToUrl \"$(cat cid.txt)\" \"$(pwd)/downloaded.txt\" false 65536 true false\n    # Wait a few seconds for the download to complete\n    ```\n\n1.  Confirm the lookup was tunnelled through Mix: node 6's log records the relay selection (SURB):\n\n    ```sh\n    grep \"Selected mix node for surbs\" storage-data/node-6/storage.log logosctl-6/logs/daemon.log\n    ```\n\n1.  Verify the round-trip: the downloaded file matches what node 5 uploaded:\n\n    ```sh\n    cat downloaded.txt\n    # Hello through Mix from the storage doc-test.\n    ```\n\n## Shut the network down\n\n1.  For each node: stop the libp2p node, destroy the storage context, and stop the daemon. The `|| true` lets the loop continue past a node that is already gone:\n\n    ```sh\n    for id in 1 2 3 4 5 6; do\n      logosctl --config-dir ./logosctl-$id call storage_module stop || true\n      logosctl --config-dir ./logosctl-$id call storage_module destroy || true\n      logosctl --config-dir ./logosctl-$id daemon stop || true\n    done\n    ```\n\n1.  Confirm the daemons have stopped:\n\n    ```sh\n    ps aux | grep logosctl | grep -v 'grep' | wc -l\n    # Should print \"0\"\n    ```\n"},{"id":"storage/get-started/troubleshooting","title":"Troubleshooting","summary":"Fix the most common problems of a storage node.","section":"storage","doc_type":"reference","url":"https://docs.logos.co/storage/get-started/faq","markdown_url":"https://docs.logos.co/storage/get-started/faq.md","permalink":"/storage/get-started/faq","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/get-started/troubleshooting.md","headings":[{"depth":2,"text":"`logosctl` fails with a FUSE error"},{"depth":2,"text":"Another application is using the listen port"},{"depth":2,"text":"I can download files, but nobody can download from me"},{"depth":2,"text":"UPnP is not working"},{"depth":2,"text":"Manual port forwarding is not working"},{"depth":2,"text":"The ports are forwarded, but the node is still unreachable"},{"depth":2,"text":"The node was reachable, but is not anymore"},{"depth":2,"text":"Downloads time out when downloading from a different machine"}],"markdown":"# Troubleshooting\n\n#### Fix the most common problems of a storage node.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3**.\n\n## `logosctl` fails with a FUSE error\n\n**Symptom**: running a tool installed from the releases fails with `No suitable fusermount binary found on the $PATH` and `Cannot mount AppImage, please check your FUSE setup`.\n\n**Cause**: on Linux, the release binaries are AppImages, which need FUSE to run. Containers (toolbox, Docker) and some minimal distributions do not ship it.\n\n**Fix**: install FUSE (`sudo dnf install fuse` on Fedora, `sudo apt install libfuse2` on Ubuntu). Without root, extract the AppImage instead and run the extracted binary:\n\n```sh\n./logosctl --appimage-extract\n./squashfs-root/AppRun --help\n```\n\n## Another application is using the listen port\n\n**Symptom**: the node fails to start, or peers cannot connect, because the TCP listen port is already taken.\n\n**Cause**: another process is occupying the port.\n\n**Fix**: stop the other process or change the listen port in the configuration. If you use manual port forwarding, update the router rule to match the new port.\n\n## I can download files, but nobody can download from me\n\n**Symptom**: downloading from the network works, but files you publish cannot be downloaded by other nodes.\n\n**Cause**: your peer is not taking inbound connections. This can happen if your node is behind a NAT and cannot locate a relay, or if the relays it can locate are fully booked. Outgoing connections (your downloads) go through your router normally; incoming connections (peers fetching from you) are blocked by it.\n\n**Fix**: your best bet is to try make your node reachable with UPnP or manual port forwarding: see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md).\n\n## UPnP is not working\n\n**Symptom**: UPnP is enabled on your router and `nat` is `auto` (the default), but the node remains unreachable.\n\n**Cause**: UPnP relies on your router supporting and enabling the UPnP protocol. Many routers have it disabled by default for security reasons.\n\n**Fix**: make sure UPnP is enabled on your router, or switch to manual port forwarding.\n\n## Manual port forwarding is not working\n\n**Symptom**: you configured port forwarding but the node remains unreachable.\n\n**Cause**: the TCP listen port is not actually open on your router.\n\n**Fix**: make sure port forwarding is enabled and matches what is in your `listen-port`. Check that rule exists and points to your machine's current local address: see [Forwarding ports manually](https://docs.logos.co/storage/concepts/connectivity.md#forwarding-ports-manually).\n\n## The ports are forwarded, but the node is still unreachable\n\n**Symptom**: UPnP or port forwarding is correctly set up on the router, but peers still cannot connect to you.\n\n**Cause**: the machine's own firewall blocks incoming connections. Some Linux distributions (such as Fedora) enable a firewall by default.\n\n**Fix**: allow inbound connections on your `<listen-port>` in your firewall. With firewalld (Fedora), for instance, you would do:\n\n```sh\nsudo firewall-cmd --permanent --add-port=<listen-port>/tcp\nsudo firewall-cmd --reload\n```\n\nWith ufw (Ubuntu):\n\n```sh\nsudo ufw allow <listen-port>/tcp\n```\n\n## The node was reachable, but is not anymore\n\n**Symptom**: the node worked for days or weeks, then peers suddenly cannot download files from you anymore.\n\n**Cause**: most home ISPs change your public IP from time to time. If `nat` is set to `extip:<IP>`, the node keeps announcing the old address.\n\n**Fix**: check your current public IP (see [Finding your public IP](https://docs.logos.co/storage/concepts/connectivity.md#finding-your-public-ip)), update the `extip` value and restart the node. To avoid this, set `nat` back to `auto` (the default) instead: the node then works out its public address by itself.\n\n## Downloads time out when downloading from a different machine\n\n**Symptom**: downloading a [CID](https://docs.logos.co/get-started/glossary.md#cid) works on the node that published it, but times out from another machine.\n\n**Cause**: NAT is blocking the peer connection: the publishing node is not reachable from the internet.\n\n**Fix**: make the publishing node reachable: see [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md).\n"},{"id":"storage/concepts/connectivity","title":"Connectivity","summary":"Understand how a storage node joins a network and becomes reachable from the internet.","section":"storage","doc_type":"concept","url":"https://docs.logos.co/storage/concepts/connectivity","markdown_url":"https://docs.logos.co/storage/concepts/connectivity.md","permalink":"/storage/concepts/connectivity","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/concepts/connectivity.md","headings":[{"depth":2,"text":"Joining a network"},{"depth":2,"text":"Being reachable: NAT"},{"depth":3,"text":"Finding your public IP"},{"depth":3,"text":"Enabling UPnP on your router"},{"depth":3,"text":"Forwarding ports manually"},{"depth":2,"text":"Ports"}],"markdown":"# Connectivity\n\n#### Understand how a storage node joins a network and becomes reachable from the internet.\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\nA node is useful only when it can reach - and be reached from - other nodes. This page explains how a node joins a network and how to make it reachable from\nthe outside.\n\n## Joining a network\n\nTo share files, you need to be part of a _Storage network_ with other peers. To\njoin a Storage network, you need to know at least one other node that is already\npart of that Storage network; that is, you need a _bootstrap peer_ for that\nstorage network. Once connected to a suitable bootstrap peer, your node should\nbe able to look up any other peers that are also part of the same storage\nnetwork. With Logos Storage, currently, you have two main choices:\n\n**Join an existing public Storage network.** We provide two sets of public\nbootstrap peers which define two logically separate, public storage networks.\nBootstrap addresses for those Storage networks are shipped with the Logos Storage\nmodule by default, and you can access them by setting the `network` option to a\npreset name. Since presets already contain the Storage network's bootstrap\nnodes, you need nothing else. If nothing is specified, Storage will always join\n`logos.test` by default.\n\n| Preset       | Description                       |\n| ------------ | --------------------------------- |\n| `logos.test` | Logos testnet (default)           |\n| `logos.dev`  | Logos devnet                      |\n\n**Create your own Storage network.** If you start your node with\n`no-bootstrap-node` set to `true`, it will bootstrap from no-one, effectively\nbecoming the bootstrap peer and only member of a new Storage network. You can\nread your node's address with the `spr` method, then use that address in the\n`bootstrap-node` option of other nodes to get them to join your Storage network.\n\n> **Note**\n>\n> Any node that joins a Storage network can be a bootstrap peer for that\n> network. Public bootstrap peers are convenient because their addresses are\n> well-known and they are highly available, but they are a convenience more than a\n> necessity.\n\n## Being reachable: NAT\n\nOn a home network, your node usually sits behind a router which shares your\nexternal IP with other devices using Network Address Translation (NAT), so it is\nnot reachable from the internet by default.\n\nWith NAT traversal, the node will try different ways to become reachable. After\nchecking the reachability of its listen port, the node will try the following\nactions, in this order, if it is unreachable:\n\n1. Try to open the ports: if the router has UPnP, NAT-PMP or PCP enabled,\n   the node asks it to open the listen port for incoming connections.\n   If that works, the node becomes reachable.\n1. Go through a relay: if the previous step fails, the node will use another\n   peer as a relay. When another peer tries to connect to this node, it will be\n   redirected to the relay, which will forward the connection to the node.\n1. Bypass the relay: ideally, when a peer arrives through the relay, the node\n   tries to open a direct connection with it anyway (hole punching). If it\n   works, the relay is dropped and the two nodes talk directly.\n\nThe reachability check is done regularly, every 2 minutes by default\n(depending on the configuration).\n\nBeing unreachable is not a dead end. Unreachable nodes sit behind the relay\nand can still share content, but the performance will be worse than for a\nreachable node.\n\n> **Info**\n>\n> * Port mapping only works if UPnP, NAT-PMP or PCP is enabled on the router. If\n> it fails, the node log will show `TCP port mapping failed` and the node will use a relay\n> instead. \n> * Relay resources are limited. You should always do your best to configure your\n> network in a way that does not require utilising a relay.\n\n> **Warning**\n>\n> If you are using [Mix](https://docs.logos.co/get-started/glossary.md#mix) with `nat:auto`, the\n> node first needs to get a reachability status, `Reachable` or `Unreachable`,\n> before it can do private downloads.\n\nThe `nat` option controls how the node handles NAT traversal:\n\n| Value        | When to use it                                                                                                                                                                                       |\n| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `auto`       | Default. Everything described above.                                                                                                                                                                   |\n| `extip:<IP>` | Set your public IP yourself, for example `extip:203.0.113.7`. The node announces that address as-is and skips the checks above. Use this when you know the public IP under which your node is reachable from; for example, when you have opened your listen port on the router yourself, or in a machine with a public IP (a cloud server or VPS). |\n\n> **Warning**\n>\n>\n> Some Linux distributions (such as Fedora) enable a firewall by default that can block incoming connections even when your port mapping is correct. You may need to allow the port through the firewall.\n>\n\n### Finding your public IP\n\nTo use `extip:<IP>` you need your public IP. Two easy ways:\n\n- From a terminal:\n\n    ```sh\n    curl -4 https://ifconfig.me\n    ```\n\n    The `-4` forces IPv4; without it, the service may return your IPv6 address.\n- From a browser: open [https://ifconfig.me](https://ifconfig.me), or check your router's admin page (usually listed as the WAN or external address).\n\n### Enabling UPnP on your router\n\nWith `nat` set to `auto` (the default), the node asks the router to open its ports by itself—but only if UPnP is enabled on the router. Router interfaces differ, but the steps are always the same:\n\n1. Open your router's admin page in a browser. Its address is your default gateway, often `192.168.1.1`. On Linux, find it with `ip route | grep default`.\n1. Find the *UPnP* setting, usually under the NAT, network, or advanced settings, and enable it.\n1. Restart the node.\n\n### Forwarding ports manually\n\nIf your router does not support UPnP, or you prefer not to enable it, map the ports yourself and announce your public IP with `extip`:\n\n1. Set a fixed value for `listen-port` (see [Ports](#ports)): you cannot forward a random port.\n1. Find your machine's address on the local network, for example with `ip -4 addr`.\n1. In your router's admin page, find the *Port forwarding* section (sometimes\n   called *NAT rules* or *Virtual server*) and add a rule pointing to your\n   machine's local address and port.\n1. Set the `nat` option to `extip:<your-public-IP>`.\n\n> **Info**\n>\n> Give your machine a fixed address on the local network (a *DHCP\n> reservation* or *static lease* in the router settings). Otherwise, forwarding\n> rules may break if the router assigns a different address to your machine.\n\n## Ports\n\nStorage requires a single TCP port, configurable through the `listen-port`\noption, to be open for it to work. This is set to `0` by default, which means\npicking a random free port. Set it to a fixed value if you want to open it on\nyour router or firewall; for example, when manually setting up forwarding rules.\n"},{"id":"storage/concepts/mix","title":"Mix","summary":"This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.","section":"storage","doc_type":"concept","url":"https://docs.logos.co/storage/concepts/mix","markdown_url":"https://docs.logos.co/storage/concepts/mix.md","permalink":"/storage/concepts/mix","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/concepts/mix.md","headings":[{"depth":2,"text":"Configuration"},{"depth":2,"text":"Enabling private downloads"}],"markdown":"# Mix\n\n> **Info**\n>\n> This document reflects the state of this Logos component as it will exist on mainnet. Some features described here may not be available on the current testnet.\n\n[Mix](https://docs.logos.co/get-started/glossary.md#mix) (short for [mix\nnetwork](https://en.wikipedia.org/wiki/Mix_network)) is a privacy layer. When\nenabled, it makes it harder for observers to figure out who is\ndownloading a file, which improves the privacy of downloaders.\n\nNormally, when a node searches and downloads content on the network, it reveals\nPersonally Identifiable Information (PII) such as the node's IP address or\npublic keys to other peers. Mix provides a way to conceal this information, making it difficult\nfor other peers to link a node requesting a file to its PII.\n\n> **Info**\n>\n> Note that, as of storage v3.0.0, only nodes _downloading_ benefit from the privacy protections of Mix.\n> Nodes providing content must remain public and identifiable.\n\n## Configuration\n\nSet `mix-enabled` to `true`. Mix needs a few extra options to know which relays it can use:\n\n| Option          | Description                                                       |\n| --------------- | ----------------------------------------------------------------- |\n| `mix-enabled`   | Turn Mix on (default `false`).                                     |\n| `dht-mix-proxy` | Peer records (SPRs) used as proxy destinations for lookups.        |\n| `mix-pool`      | Path to a JSON file listing the Mix relays.                        |\n| `mix-pool-json` | The relay list as inline JSON. Takes precedence over `mix-pool`.   |\n\nExample:\n\n```json\n{\n  \"mix-enabled\": true,\n  \"mix-pool\": \"/path/to/mix-pool.json\"\n}\n```\n\n## Enabling private downloads\n\nWhen Mix is configured (`mix-enabled` true and at least one `dht-mix-proxy`\nset), you can set the `isPrivate` flag in download operations like\n`downloadToUrl` and `downloadChunks` (see [API\nreference](https://logos-co.github.io/logos-storage-module/v3.0.0-rc1/api_reference.html))\nso that downloads happen over mix. This in practice makes your download\nprivacy-preserving; that is, it is difficult for third parties to tell that you\nare downloading a file, even if they are serving the file to you themselves.\n\n**Advertisements.** Logos storage's download operations expose a second option,\n`advertise`, which controls whether or not a node advertises on the network that\nit has a given file so that other peers can download from it. To benefit from the privacy protections provided by the mix network, you should set `advertise=false` every time you set `isPrivate=true`.\n\n> **Warning**\n>\n> When using `nat:auto`, the node first needs to get a reachability status,\n> `Reachable` or `Unreachable`, before it can do private downloads. See\n> [Connectivity](https://docs.logos.co/storage/concepts/connectivity.md) for details.\n"},{"id":"storage/build-app/build-cli-app-that-uses-storage-api","title":"Build a CLI app that uses the Storage API","summary":"Wrap the Storage module API in a simple synchronous CLI interface.","section":"storage","doc_type":"procedure","url":"https://docs.logos.co/storage/build-app/build-cli-app-that-uses-storage-api","markdown_url":"https://docs.logos.co/storage/build-app/build-cli-app-that-uses-storage-api.md","permalink":"/storage/build-app/build-cli-app-that-uses-storage-api","source":"https://github.com/logos-co/logos-docs/blob/main/docs/storage/build-app/build-cli-app-that-uses-storage-api.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Scaffold the module project"},{"depth":2,"text":"Step 2: Configure the module metadata, flake, and CMake files"},{"depth":2,"text":"Step 3: Define the module interface"},{"depth":2,"text":"Step 4: Set up shared state and helper functions"},{"depth":2,"text":"Step 5: Implement the synchronous transfer helper"},{"depth":2,"text":"Step 6: Implement the context hook and the public operations"},{"depth":2,"text":"Step 7: Build your module"},{"depth":2,"text":"Step 8: Start the Logos daemon"},{"depth":2,"text":"Step 9: Install the Storage module and your module"},{"depth":2,"text":"Step 10: Load the CLI module"},{"depth":2,"text":"Step 11: Publish a file"},{"depth":2,"text":"Step 12: Download a file"}],"markdown":"# Build a CLI app that uses the Storage API\n\n#### Wrap the Storage module API in a simple synchronous CLI interface.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.3.0**.\n\nThis tutorial uses **Storage module v3.0.2**, **Logos module builder 0.3.2**, and `logosctl 0.3.2`.\n\nThe [Storage Module API](https://logos-co.github.io/logos-storage-module/latest/api_reference.html) offers a comprehensive way to access the Storage module, but can be inconveniently complex for CLI access. This tutorial builds a wrapper [module](https://docs.logos.co/get-started/glossary.md#module)—a separate module that depends on [Logos Storage](https://docs.logos.co/get-started/glossary.md#logos-storage) and exposes a simpler, synchronous interface over it. It is intended for developers building custom Logos modules who want a straightforward CLI-style interface instead of working with the Storage module's asynchronous API directly.\n\n> **Info: Prerequisites**\n>\n> - A supported OS\n>     - Linux\n>     - Mac OS (should work, but not tested)\n> - **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>   ```bash\n>   mkdir -p ~/.config/nix\n>   echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>   ```\n>\n> - **Git**\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n\n## What to expect\n\n- You can scaffold a Logos module that wraps the Storage module's asynchronous API in a synchronous interface.\n- You can publish a local file to the Logos Storage network with a single `storage_cli publish` command.\n- You can download a file from the network with a single `storage_cli download` command.\n\n## Step 1: Scaffold the module project\n\nUse the Logos module builder template to scaffold a new module project:\n\n```bash\nmkdir ./storage_cli\ncd ./storage_cli\ngit init\nnix flake init -t github:logos-co/logos-module-builder/0.3.2\n```\n\n## Step 2: Configure the module metadata, flake, and CMake files\n\n1.  Replace the contents of `metadata.json` with the following. It declares a dependency on `storage_module` and sets `concurrency` so the module's calls run off the main event loop:\n\n    ```json showLineNumbers\n    {\n      \"name\": \"storage_cli\",\n      \"display_name\": \"Simple Storage CLI\",\n      \"version\": \"1.0.0\",\n      \"type\": \"core\",\n      \"interface\": \"universal\",\n      \"category\": \"example\",\n      \"description\": \"A simple CLI frontend module to Logos Storage\",\n      \"main\": \"storage_cli_plugin\",\n      \"dependencies\": [\"storage_module\"],\n      \"concurrency\": \"multi\",\n      \"nix\": {\n        \"packages\": {\n          \"build\": [],\n          \"runtime\": []\n        },\n        \"external_libraries\": [],\n        \"cmake\": {\n          \"find_packages\": [],\n          \"extra_sources\": [],\n          \"extra_include_dirs\": [],\n          \"extra_link_libraries\": []\n        }\n      }\n    }\n    ```\n\n    - `\"dependencies\": [\"storage_module\"]` declares that this module depends on the Storage module.\n    - `\"concurrency\": \"multi\"` runs the request handler on a thread separate from the single-threaded event loop, which makes it easier to turn the asynchronous Storage API calls into synchronous ones later.\n\n1.  Delete the generated placeholder implementation, since you'll provide your own:\n\n    ```bash\n    rm src/minimal_impl.{h,cpp}\n    ```\n\n1.  In the generated `flake.nix`, replace the `inputs` block with the following pinned inputs. Keep the generated `outputs` as they are:\n\n    ```nix\n    {\n      inputs = {\n        logos-module-builder.url = \"github:logos-co/logos-module-builder/0.3.2\";\n        storage_module.url = \"github:logos-co/logos-storage-module/v3.0.2\";\n      };\n\n      # ...\n    }\n    ```\n\n1.  Edit `CMakeLists.txt` to reference the new source file names. Replace `logos_module` with:\n\n    ```cmake\n    # Define the module. The generated glue is compiled automatically.\n    logos_module(\n        NAME ${MODULE_NAME}\n        SOURCES\n            src/storage_cli_impl.h\n            src/storage_cli_impl.cpp\n    )\n    ```\n\n## Step 3: Define the module interface\n\nCreate `src/storage_cli_impl.h` with the following interface. It declares two operations—`publish` and `download`—both of which return a [`StdLogosResult`](https://github.com/logos-co/logos-cpp-sdk/tree/95d7b3a9c5ef845bdc31f12f1d8222a12eda916d#logosresult) ([result type](https://en.wikipedia.org/wiki/Result_type)), and overrides `onContextReady`, a [Logos C++ SDK](https://github.com/logos-co/logos-cpp-sdk) hook called when the module is loaded:\n\n```cpp showLineNumbers\n#pragma once\n\n#include <logos_module_context.h>\n#include <logos_result.h>\n#include <string>\n\n/**\n * A synchronous, CLI-shaped facade over the asynchronous `storage_module`.\n */\nclass StorageCliImpl : public LogosModuleContext {\npublic:\n  /**\n   * Uploads a file to the local node and returns the completion result.\n   */\n  StdLogosResult publish(const std::string &input);\n  /**\n   * Downloads a file from the network onto the specified local path.\n   * Returns the completion result.\n   */\n  StdLogosResult download(const std::string &cid, const std::string &output);\n\nprotected:\n  /// Starts (and configures) the storage node when the module is loaded.\n  void onContextReady() override;\n};\n```\n\n## Step 4: Set up shared state and helper functions\n\nThe rest of the implementation goes in `src/storage_cli_impl.cpp`. Add the file's includes, then the shared state and small helper functions the rest of the module will use.\n\n1.  Start `src/storage_cli_impl.cpp` with its header and includes. `nlohmann/json` ships with the Logos SDK, so you don't need to install it separately:\n\n    ```cpp showLineNumbers\n    #include \"storage_cli_impl.h\"\n    #include \"logos_sdk.h\"\n\n    #include <algorithm>\n    #include <cstdint>\n    #include <filesystem>\n    #include <functional>\n    #include <future>\n    #include <iostream>\n    #include <mutex>\n    #include <system_error>\n    #include <nlohmann/json.hpp>\n\n    using nlohmann::json;\n    ```\n\n1.  Open an anonymous namespace and define the node configuration and transfer chunk size:\n\n    ```cpp showLineNumbers=16\n    namespace {\n\n    constexpr const char *kNodeConfig = R\"({\n        \"log-level\": \"INFO\",\n        \"nat\": \"auto\",\n        \"data-dir\": \"/tmp/logos-storage\",\n        \"network\": \"logos.test\"\n    })\";\n\n    constexpr int64_t kChunkSize = 65536;\n    ```\n\n1.  Add standard C++ [promises](https://en.cppreference.com/cpp/thread/promise) to make the `publish`/`download` operations synchronous and a [mutex](https://en.cppreference.com/cpp/thread/mutex) to serialise these operations:\n\n    ```cpp showLineNumbers=27\n    // We'll use two promises: one for synchronising node startup, and another for\n    // upload/download operation results.\n    std::promise<bool> gStarted;\n    // The start future is potentially called by several different threads, so we\n    // need a shared future.\n    std::shared_future<bool> gStartedFut = gStarted.get_future().share();\n    // A rejected start can also emit a completion event. Resolve startup only once.\n    std::once_flag gStartedOnce;\n    // gResult is initialised during an asynchronous operation dispatch, set in\n    // the callback once, and consumed by the dispatcher exactly once, so we can\n    // use a regular future.\n    std::promise<std::string> gResult;\n    // Serialises upload/download operations.\n    std::mutex gOpLock;\n\n    int64_t gTransferBytes = 0;\n    int64_t gTransferTotal = 0;\n    ```\n\n    - `gTransferTotal` and `gTransferBytes` track the progress of the single `publish`/`download` operation this module allows to run at a time.\n\n1.  Add helper functions for printing transfer progress and for parsing JSON payloads returned by the Storage module:\n\n    ```cpp showLineNumbers=45\n    void echo(const std::string &line, bool endline = true) {\n      std::cout << line;\n      if (endline) {\n        std::cout << '\\n';\n      }\n      std::cout.flush();\n    }\n\n    void printProgress() {\n      if (gTransferTotal == 0) {\n        echo(\" \" + std::to_string(gTransferBytes) + \" bytes\");\n        return;\n      }\n      int64_t completed = std::min(gTransferBytes, gTransferTotal);\n      echo(\"  \" + std::to_string(completed * 100 / gTransferTotal) + \"% (\" +\n           std::to_string(completed) + \" of \" + std::to_string(gTransferTotal) +\n           \" bytes)\");\n    }\n\n    json parsePayload(const std::string &payload) {\n      json j = json::parse(payload, nullptr, false);\n      if (!j.is_object()) {\n        echo(\"Failed to parse payload: \" + payload);\n        return json::object();\n      }\n      return j;\n    }\n    ```\n\n## Step 5: Implement the synchronous transfer helper\n\n1.  Add the `onProgress` and `onDone` callbacks. Both [`uploadUrl`](https://logos-co.github.io/logos-storage-module/latest/api_reference.html#_CPPv4N17StorageModuleImpl9uploadUrlERKNSt6stringE7int64_tb) and [`downloadToUrl`](https://logos-co.github.io/logos-storage-module/latest/api_reference.html#_CPPv4N17StorageModuleImpl13downloadToUrlERKNSt6stringERKNSt6stringEb7int64_tbb) report progress and completion through these callbacks. Progress events contain a byte increment and the total size. `onStarted` resolves the startup promise once, regardless of the result:\n\n    ```cpp showLineNumbers=73\n    void onProgress(const std::string &payload) {\n      const json j = parsePayload(payload);\n      gTransferBytes += j.value(\"bytes\", int64_t{0});\n      gTransferTotal = j.value(\"total\", gTransferTotal);\n      printProgress();\n    }\n\n    void onDone(const std::string &payload) { gResult.set_value(payload); }\n\n    void onStarted(bool success) {\n      std::call_once(gStartedOnce, [success] { gStarted.set_value(success); });\n    }\n    ```\n\n1.  Add `syncTransferOp`, the helper that turns an asynchronous Storage operation into a synchronous one - this is the core of the module:\n\n    ```cpp showLineNumbers=86\n    StdLogosResult syncTransferOp(const std::string &what, int64_t total,\n                                  const std::function<StdLogosResult()> &op) {\n      echo(\"Waiting for node to start.\");\n      if (!gStartedFut.get()) {\n        return StdLogosResult{\n            .success = false, .value = {}, .error = \"Node start failed\"};\n      }\n      echo(\"Node is started, attempting to run \" + what + \" operation.\");\n\n      // This will block attempts to run multiple operations at once.\n      // This is not a limitation in storage but of our state tracking.\n      std::scoped_lock lock(gOpLock);\n\n      gResult = std::promise<std::string>();\n      gTransferBytes = 0;\n      gTransferTotal = total;\n\n      // Actually sends the operation to Storage.\n      StdLogosResult started = op();\n      if (!started.success) {\n        return started;\n      }\n\n      const std::string result = gResult.get_future().get();\n      const json payload = parsePayload(result);\n      const bool success = payload.value(\"success\", false);\n      return StdLogosResult{.success = success,\n                            .value = payload,\n                            .error = success ? \"\" : payload.value(\"error\", \"Transfer failed\")};\n    }\n\n    } // namespace\n    ```\n\n    - `gStartedFut` waits until node startup succeeds or fails.\n    - `gOpLock` serialises transfers because this example shares one result promise and progress counters.\n    - The helper resets that state, dispatches `op()`, and waits for `onDone` to fulfil the result promise.\n    - The returned result includes the completion payload and any transfer error.\n\n## Step 6: Implement the context hook and the public operations\n\n1.  Implement `onContextReady`, which registers the Storage module's callbacks and starts the Storage node:\n\n    ```cpp showLineNumbers=119\n    void StorageCliImpl::onContextReady() {\n      StorageModule &storage = modules().storage_module;\n\n      storage.onStorageStart([](const std::string &payload) {\n        const json j = parsePayload(payload);\n        echo(\"Node started with result: \" + j.dump());\n        onStarted(j.value(\"success\", false));\n      });\n\n      storage.onStorageUploadProgress(&onProgress);\n      storage.onStorageDownloadProgress(&onProgress);\n      storage.onStorageUploadDone(&onDone);\n      storage.onStorageDownloadDone(&onDone);\n\n      echo(\"starting storage node (data-dir /tmp/logos-storage, network \"\n           \"logos.test)...\");\n\n      if (storage.isRunning()) {\n        onStarted(true);\n        return;\n      }\n      if (!storage.init(kNodeConfig)) {\n        echo(\"failed to initialise storage module\");\n        onStarted(false);\n        return;\n      }\n      if (!storage.start()) {\n        echo(\"node start was rejected\");\n        onStarted(false);\n      }\n    }\n    ```\n\n    > **Note**\n    >\n    > * To make the code in this tutorial simpler, failed initialisation or a rejected start make all subsequent transfers return `Node start failed`. You will need to reload the module to try again.\n    > * The Storage module stores its configuration in `~/.logos_storage/config.json`.\n\n1.  Implement `publish`, which uploads a local file. The final `true` argument to `uploadUrl` enables advertising and serving the file to peers:\n\n    ```cpp showLineNumbers=151\n    StdLogosResult StorageCliImpl::publish(const std::string &input) {\n      // Paths are resolved relative to the daemon process. Callers\n      // should use absolute paths.\n      std::error_code ec;\n      const std::filesystem::path path = std::filesystem::absolute(input, ec);\n      if (ec) {\n        return {.success = false, .value = {}, .error = ec.message()};\n      }\n      const auto size = static_cast<int64_t>(std::filesystem::file_size(path, ec));\n      if (ec) {\n        echo(\"upload failed: cannot read \" + input + \" (\" + ec.message() + \")\");\n        return {.success = false, .value = {}, .error = ec.message()};\n      }\n\n      echo(\"uploading \" + path.string() + \" (\" + std::to_string(size) + \" bytes)\");\n\n      return syncTransferOp(\"upload\", size, [&] {\n        return modules().storage_module.uploadUrl(path.string(), kChunkSize, true);\n      });\n    }\n    ```\n\n1.  Implement `download`, which downloads a file by its [CID](https://docs.logos.co/get-started/glossary.md#cid) onto local disk. The last two arguments to `downloadToUrl` select a direct download; i.e, not using the [Logos mix network](https://docs.logos.co/storage/concepts/mix.md) (`isPrivate=false`), and enable advertising and serving the downloaded data (`advertise=true`); i.e, your node will actively replicate and serve it:\n\n    ```cpp showLineNumbers=171\n    StdLogosResult StorageCliImpl::download(const std::string &cid,\n                                            const std::string &output) {\n      std::error_code ec;\n      const std::filesystem::path path = std::filesystem::absolute(output, ec);\n      if (ec) {\n        echo(\"download failed: bad output path \" + output + \" (\" + ec.message() +\n             \")\");\n        return {.success = false, .value = {}, .error = ec.message()};\n      }\n\n      echo(\"Downloading \" + cid + \" to \" + path.string());\n\n      return syncTransferOp(\"download\", 0, [&] {\n        return modules().storage_module.downloadToUrl(cid, path.string(), false,\n                                                      kChunkSize, false, true);\n      });\n    }\n    ```\n\n## Step 7: Build your module\n\nBuild the module:\n\n```bash\ngit add flake.nix metadata.json CMakeLists.txt src\nnix build '.#lgx-portable'\ngit add flake.lock\n```\n\n- **Expected result:** an `.lgx` [package](https://docs.logos.co/get-started/glossary.md#package) appears under the `result` folder:\n\n  ```bash\n  $ ls result\n  logos-storage_cli-module-lib.lgx\n  ```\n\n## Step 8: Start the Logos daemon\n\nPackage installs are handled by a module bundled inside the daemon, so the daemon has to be running before you install anything.\n\nFrom the `storage_cli` project directory you have been working in since Step 1, start `logosctl`, using a project-local session so installed modules and daemon logs stay under `./config-dir`. The example stores file data separately in `/tmp/logos-storage`:\n\n```bash\nlogosctl daemon start --detach --config-dir ./config-dir\n```\n\n## Step 9: Install the Storage module and your module\n\nThe Storage module is a dependency of your module, so install it first.\n\n1.  Refresh the [catalogue](https://docs.logos.co/get-started/glossary.md#catalogue) and install the Storage module package by name:\n\n    ```bash\n    logosctl --config-dir ./config-dir catalog refresh\n    logosctl --config-dir ./config-dir package install storage_module --version 3.0.2 --yes\n    ```\n\n1.  Install your own module package from the local `.lgx` file:\n\n    ```bash\n    logosctl --config-dir ./config-dir package install --file ./result/logos-storage_cli-module-lib.lgx --yes\n    ```\n\n    - Without `--yes`, `package install` asks for confirmation (`Proceed? [y/N]`), and fails with `Refusing to proceed without confirmation` when it cannot prompt (for example, in a script).\n\n    - `package install` re-scans and unpacks into the session's own `modules/` directory (`./config-dir/modules/` here), so the daemon picks up both modules immediately—no restart needed.\n\n## Step 10: Load the CLI module\n\n1.  Confirm both modules are installed:\n\n    ```bash\n    logosctl --config-dir ./config-dir daemon status\n    ```\n\n    - **Expected result:**\n\n      ```text\n      Logosctl Daemon\n        Status:       running\n        PID:          405049\n        Uptime:       0s\n        Version:      v1.0.0\n\n      Modules: 5 loaded, 0 crashed, 1 not loaded\n        storage_cli         v1.0.0  not_loaded  -\n        package_manager     v1.0.0  loaded      13s\n        storage_module      v3.0.2  loaded      1s\n        package_downloader  v1.0.0  loaded      13s\n        modules_state       v0.1.0  loaded      13s\n        capability_module   v1.0.0  loaded      13s\n      ```\n\n1.  Load the CLI module:\n\n    ```bash\n    logosctl --config-dir ./config-dir module load storage_cli\n    ```\n\n    - **Expected result:**\n\n      ```text\n      Loaded module: storage_cli (v1.0.0)\n      ```\n\n## Step 11: Publish a file\n\nCreate a sample file and publish it with the CLI module. Pass absolute paths because the daemon will otherwise resolves file paths relative to its own working directory:\n\n```bash\necho \"Hello, World!\" > hello.txt\nlogosctl --config-dir ./config-dir call storage_cli publish \"$PWD/hello.txt\"\n```\n\n- **Expected result:**\n\n  ```json\n  {\n    \"error\": null,\n    \"success\": true,\n    \"value\": {\n      \"cid\": \"zDvZRwzkx14BXkvr6MBpY7e29GgP3QBYM671u1P1kiLGYxt2iyHA\",\n      \"sessionId\": \"0\",\n      \"success\": true\n    }\n  }\n  ```\n\n- Because the `onProgress` callback runs inside the daemon process, progress logs appear in the daemon's own log file (`./config-dir/logs/daemon.log`), not here. For a small file like this, progress is a single line:\n\n  ```text\n  [2026-08-19 18:58:13.540] [out] [storage_cli]   100% (14 of 14 bytes)\n  ```\n\n- You can share the CID that appears after your call to `publish` with other people, and they should be able to download it like we describe in the next step.\n\n## Step 12: Download a file\n\nDownload [Farewell to Westphalia](https://logos.co/book/farewell-to-westphalia-foss-edition.pdf) from the Storage network by its CID:\n\n```bash\nlogosctl --config-dir ./config-dir call storage_cli download zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ \"$PWD/farewell-to-westphalia.pdf\"\n```\n\nThis may take a little while. On a node that has just started, the first attempts can fail after about 20 seconds, with `call to 'storage_cli.download' timed out after 20000ms`, `\"error\":\"expected a result object, got null\"`, or `RPC_FAILED`, because the node is still looking up the file's manifest on the network. A CLI timeout does not cancel the transfer. Check `./config-dir/logs/daemon.log` and wait for the pending operation to finish before retrying. If it remains stuck, stop and restart the daemon, then load `storage_cli` again.\n\n- **Expected result:**\n\n  ```json\n  {\n    \"error\": null,\n    \"success\": true,\n    \"value\": {\n      \"sessionId\": \"zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ\",\n      \"success\": true\n    }\n  }\n  ```\n\n- The daemon logs show download progress, including the total size:\n\n  ```text\n  [2026-10-08 15:41:51.980] [out] [storage_cli] Downloading zDvZRwzkzrrYB6sS1rRpRLt4gBhc1pWoyTSjkfszfmj1seaYYLCZ to /path/to/storage_cli/farewell-to-westphalia.pdf\n  [2026-10-08 15:41:51.980] [out] [storage_cli] Waiting for node to start.\n  [2026-10-08 15:41:51.980] [out] [storage_cli] Node is started, attempting to run download operation.\n  [2026-10-08 15:41:51.993] [out] [storage_cli]   2% (65536 of 2276462 bytes)\n  ...\n  [2026-10-08 15:41:52.005] [out] [storage_cli]   100% (2276462 of 2276462 bytes)\n  ```\n\nThe public download should produce a 2,276,462-byte PDF.\n\n> **Tip**\n>\n> Try uploading different files and sharing their `CIDs` with other network participants if you know anyone. They should be able to download them.\n\nYou may now stop the daemon (`logosctl --config-dir ./config-dir daemon stop`), or leave it running and use it for other operations.\n"},{"id":"mixnet/get-started/send-anonymised-messages-over-the-mix-network","title":"Send anonymised messages over the mix network","summary":"Get started sending and receiving messages with improved sender privacy using the Logos Chat UI.","section":"mixnet","doc_type":"procedure","url":"https://docs.logos.co/mixnet/get-started/send-anonymised-messages-over-the-mix-network","markdown_url":"https://docs.logos.co/mixnet/get-started/send-anonymised-messages-over-the-mix-network.md","permalink":"/mixnet/get-started/send-anonymised-messages-over-the-mix-network","source":"https://github.com/logos-co/logos-docs/blob/main/docs/mixnet/get-started/send-anonymised-messages-over-the-mix-network.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Start the first instance and connect to the mix fleet"},{"depth":2,"text":"Start a second instance and exchange messages"},{"depth":2,"text":"Troubleshooting mix chat"},{"depth":3,"text":"Why does the app fail to launch after a previous run?"},{"depth":3,"text":"Why does the mix pool stay at `MIX 0/4` or fill slowly?"},{"depth":3,"text":"Why does the send succeed but the recipient never sees the message?"},{"depth":3,"text":"Why do two instances on the same machine collide?"}],"markdown":"# Send anonymised messages over the mix network\n\n#### Get started sending and receiving messages with improved sender privacy using the Logos Chat UI.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.2.1**.\n\nThis procedure covers how to run the [`chat_ui_mix`](https://github.com/logos-co/logos-chat-ui/tree/feat/logos-testnetv02-mix) desktop app, connect to the testnet-0.2 [mix](https://docs.logos.co/get-started/glossary.md#mix) fleet, and exchange messages between two instances with sender unlinkability. No configuration is required—the [app](https://docs.logos.co/get-started/glossary.md#app) ships with shared testnet credentials and discovers the fleet automatically.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux\n>     - macOS\n> - Outbound TCP access to the fleet on port `30304`.\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n\n## What to expect\n\n- You can connect to the testnet-0.2 mix fleet and watch the status bar reach **MIX 5/4**, which confirms the mix node pool is ready for privacy-preserving sends.\n- You can exchange intro bundles between two instances and send messages that are routed sender-unlinkably through the 3-hop [mixnet](https://docs.logos.co/get-started/glossary.md#mixnet).\n- You can confirm end-to-end delivery by matching the sender's `Message sent via mix successfully` log with the recipient's `ChatBackend: New message: …` log.\n\n## Start the first instance and connect to the mix fleet\n\nThe mix chat app can be found in the `logos-chat-ui` repository.\n\n1. Clone the repository and check out the `feat/logos-testnetv02-mix` branch:\n\n   ```sh\n   git clone https://github.com/logos-co/logos-chat-ui.git\n   cd logos-chat-ui\n   git checkout feat/logos-testnetv02-mix\n   ```\n\n1. Start the app:\n\n   ```sh\n   nix run --accept-flake-config\n   ```\n\n   > **Info**\n   >\n   > The first build pulls the full dependency chain through Nix and takes approximately 15–20 minutes. Subsequent launches use the cache and start in seconds.\n\n1. In the startup popup, pick a demo user (1–10) and click **Start**.\n\n   - The app loads the bundled testnet credentials and begins discovering the fleet mix nodes via libp2p Kademlia service discovery.\n\n1. Wait for the mix pool to reach the minimum. Check the status bar and console for the following:\n\n   - Status bar shows `Connected`, `MIX: ENABLED`, and `USER: <n>` immediately after launch. Console shows `Kademlia mix discovery mounted` and `Connected to static peers`.\n   - Over the next 1–2 minutes the mix pool fills. Status bar moves from `MIX 0/4` toward **`MIX 5/4`** (pool size 5 / minimum 4). Console shows `Waiting for mix node pool` then **`Mix node pool ready  poolSize=5`**.\n   - The send box remains disabled until the pool reaches the minimum.\n\n## Start a second instance and exchange messages\n\nRun the second instance in its own folder with a different demo user and port to avoid credential collisions and RLN rate-limit conflicts.\n\n> **Warning**\n>\n> Never run two instances in the same working directory. The app stages credential files (`rln_membership.json`, `rln_tree.db`, and others) into the current folder; two instances sharing the same folder race on identity. If a previous launch left orphaned processes behind, run `pkill -9 -f logos_host_qt` before starting a new instance.\n\n1. In a new terminal, create a separate working directory for the second instance and start it:\n\n   ```sh\n   mkdir ../chat2 && cd ../chat2 # ../ steps out of the logos-chat-ui clone\n   CHAT_DEMO_USER=2 CHAT_PORT=62002 nix run ../logos-chat-ui --accept-flake-config\n   ```\n\n   - `CHAT_DEMO_USER` pre-selects a different demo user (use any index other than the one chosen in the first instance). The startup popup still opens with that user selected: click **Start** to connect.\n   - `CHAT_PORT` sets the libp2p listen port to avoid a port conflict with the first instance (`0` selects a random port).\n   - Wait for this instance's status bar to also reach **`MIX 5/4`** before sending.\n\n1. Exchange intro bundles between the two instances:\n\n   - In one instance, click **Get Intro Bundle**, then click **Copy to Clipboard** in the **My Bundle** dialogue that opens.\n   - In the other instance, click **+ new**, paste the bundle and enter a first message, then click **Create**.\n\n1. Send a message from one instance and confirm it arrives in the other:\n\n   - **Sender console:** `Sending via mix (lightpushPublish)` → **`Message sent via mix successfully`**\n   - **Recipient console:** `ChatBackend: New message: …`\n   - The message appears in the recipient's conversation within approximately 1–2 seconds.\n   - A `Mix lightpush: no SURB reply within 60s` warning in the sender console is benign—delivery uses the forward path, which is independent of the SURB reply.\n\n1. Confirm the final status bar state on both instances:\n\n   - Status bar reads **`MIX 5/4`** and **`Connected`**.\n\n## Troubleshooting mix chat\n\n### Why does the app fail to launch after a previous run?\n\nA previous instance was killed while `logos_host_qt` child processes were still running. These orphaned processes block the next launch. Run `pkill -9 -f logos_host_qt` to clear them, then try again.\n\n### Why does the mix pool stay at `MIX 0/4` or fill slowly?\n\nThe pool takes 1–2 minutes to fill on first launch. Wait and watch the status bar. If the pool does not reach **`MIX 5/4`** after several minutes, the fleet may be temporarily degraded—the mix fleet is a shared testnet of 5 nodes and connectivity depends on all of them being reachable.\n\n### Why does the send succeed but the recipient never sees the message?\n\nThe app receives as a light client and must stay subscribed to a healthy fleet node. If fleet connectivity is degraded, the round-trip may not complete even though the send path works. Retry after confirming the pool is at **`MIX 5/4`** on both instances. If the problem persists, the fleet may be temporarily unavailable.\n\n### Why do two instances on the same machine collide?\n\nEither both instances share the same working directory, the same demo-user index, or both are listening on the same port. Run each instance in its own folder (`chat2`, and so on), use a different `CHAT_DEMO_USER` value for each, and set distinct `CHAT_PORT` values.\n"},{"id":"peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api","title":"Build a Logos Core module that uses the Service Discovery API","summary":"Get started with typed, service-keyed peer lookups in a live Logos network.","section":"peer-discovery","doc_type":"procedure","url":"https://docs.logos.co/peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api","markdown_url":"https://docs.logos.co/peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api.md","permalink":"/peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api","source":"https://github.com/logos-co/logos-docs/blob/main/docs/peer-discovery/get-started/build-logos-core-module-that-uses-service-discovery-api.md","headings":[{"depth":2,"text":"What to expect"},{"depth":2,"text":"Step 1: Smoke-test the bundled example binary (Optional)"},{"depth":2,"text":"Step 2: Scaffold the new Logos Core module"},{"depth":2,"text":"Step 3: Build both modules"},{"depth":2,"text":"Step 4: Load the modules and verify the single-node flow"},{"depth":2,"text":"Step 5: Run three-node local discovery"},{"depth":2,"text":"Troubleshooting service discovery"},{"depth":3,"text":"Why does `discover` return `[]` even after waiting?"}],"markdown":"# Build a Logos Core module that uses the Service Discovery API\n\n#### Get started with typed, service-keyed peer lookups in a live Logos network.\n\n> **Tip: Version**\n>\n> This document is accurate for **Testnet v0.2.1**.\n\nApplications on the Logos network need a protocol-agnostic way to find peers offering specific services—[mix](https://docs.logos.co/get-started/glossary.md#mix) nodes, relay nodes, storage providers—at runtime without hard-coding topology or peer lists. The Service Discovery API enables any Logos Core [module](https://docs.logos.co/get-started/glossary.md#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. This procedure covers how to write and run 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.\n\n> **Info: Prerequisites**\n>\n>\n> - A supported OS:\n>     - Linux: Ubuntu 22.04+\n>     - macOS: 14+\n> - 2 GB RAM (sufficient for a local two-module test)\n> - **Nix** with flakes enabled.\n>    - Install from [nixos.org](https://nixos.org/download.html), then enable flakes:\n>\n>    ```bash\n>    mkdir -p ~/.config/nix\n>    echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\n>    ```\n> - [`logosctl`](https://github.com/logos-co/logos-logoscore-cli/releases/tag/0.3.2) installed.\n>    - Install it by running `curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh`\n\n## What to expect\n\n- You can advertise a named service from a Logos Core module and discover peers offering the same service via the Kad-DHT, without hard-coding peer addresses.\n- You can verify the full discovery flow locally across three `logosctl` daemon instances, each with its own session and listen port.\n- You have a reusable module scaffold with typed `disco*` wrappers that you can extend for production service types.\n\n## Step 1: Smoke-test the bundled example binary (Optional)\n\nBuild and run the self-contained two-node demo to confirm the module and its C bindings are working before writing your own module.\n\n1. Clone the repository and enter the project directory:\n\n   ```sh\n   git clone https://github.com/logos-co/logos-libp2p-module\n   cd logos-libp2p-module\n   ```\n\n1. Build the module:\n\n   ```sh\n   nix build -L\n   ```\n\n   > **Info**\n   >\n   > The first-run Nix build can take 5–20 minutes to fetch dependencies; subsequent builds are cached. Estimated total time is 15–25 minutes.\n   \n1. Build the service discovery tutorial from the root project and run it. The Nix development shell provides the C-binding header and library, so nothing needs to be copied into `./lib`:\n\n   ```sh\n   nix develop --command bash -c 'cmake -B build -S . && cmake --build build --target tutorial_9_service_discovery -j'\n   nix develop --command ./build/tutorial/tutorial_9_service_discovery\n   ```\n\n   Expected output (between the node's log lines):\n\n   ```\n   === Tutorial 9: Service Discovery ===\n\n   Bootstrap node started: 12D3KooW...\n   Advertiser connected to bootstrap\n   Discoverer connected to bootstrap\n\n   Advertiser advertising: \"demo-chat-service\"\n   Advertising started\n   Discoverer registering interest in \"demo-chat-service\"\n   Discoverer looking up \"demo-chat-service\"...\n   Discoverer found 1 provider(s):\n     Peer: 12D3KooW...\n       Service: demo-chat-service (data: version=1.0;capacity=100)\n\n   Random lookup by advertiser...\n   Found 2 random peer(s):\n   ...\n   === Tutorial 9 Complete ===\n   ```\n\n   - [Peer IDs](https://docs.logos.co/get-started/glossary.md#peer-id) are non-deterministic across runs.\n\n   > **Info**\n   >\n   > The tutorial runs a bootstrap node plus an advertiser and a discoverer, so the discoverer finds the advertiser through the DHT—a successful run prints `found 1 provider(s)`. Exact peer counts and IDs vary per run.\n\n## Step 2: Scaffold the new Logos Core module\n\nRun the scaffold tool from the parent directory to generate the module skeleton, then declare the `libp2p_module` dependency.\n\n1. From the parent directory, scaffold the module:\n\n   ```sh\n   cd ..\n   nix run github:logos-co/logos-dev-boost -- init my_service_module --type module\n   cd logos-my-service-module\n   ```\n\n   - The scaffold tool prefixes the output directory with `logos-`, producing `logos-my-service-module`.\n\n1. Replace the contents of `metadata.json` with the following to declare the `libp2p_module` dependency:\n\n   ```json\n   {\n     \"name\": \"my_service_module\",\n     \"version\": \"1.0.0\",\n     \"description\": \"Service discovery demo module\",\n     \"type\": \"core\",\n     \"interface\": \"universal\",\n     \"main\": \"my_service_module_plugin\",\n     \"dependencies\": [\"libp2p_module\"]\n   }\n   ```\n\n1. Replace the contents of `src/my_service_module_impl.h` with the following:\n\n   ```cpp\n   // my_service_module_impl.h — inherit LogosModuleContext:\n   #pragma once\n   #include <string>\n   #include \"logos_module_context.h\"\n\n   class MyServiceModuleImpl : public LogosModuleContext {\n   public:\n       std::string startDiscovery();\n       std::string getPeerInfo();\n       std::string advertise(const std::string& serviceId, const std::string& serviceData);\n       std::string discover(const std::string& serviceId);\n       std::string stopDiscovery();\n   };\n   ```\n\n1. Replace the contents of `src/my_service_module_impl.cpp` with the following:\n\n   ```cpp\n   #include \"my_service_module_impl.h\"\n   #include \"logos_sdk.h\"   // generated; defines: struct LogosModules { Libp2pModule libp2p_module; };\n   #include <thread>\n   #include <chrono>\n\n   std::string MyServiceModuleImpl::startDiscovery() {\n       auto r = modules().libp2p_module.start();\n       if (!r.success) return \"start failed: \" + r.error;\n       auto r2 = modules().libp2p_module.discoStart();\n       if (!r2.success) return \"discoStart failed: \" + r2.error;\n       return \"discovery started\";\n   }\n\n   // Returns this node's own peer record (peerId + listen addrs). Use it on a\n   // bootstrap node to obtain the peerId/addr that other instances bootstrap against.\n   std::string MyServiceModuleImpl::getPeerInfo() {\n       auto r = modules().libp2p_module.peerInfo();\n       if (!r.success) return \"peerInfo failed: \" + r.error;\n       return r.value.is_string() ? r.value.get<std::string>() : r.value.dump();\n   }\n\n   std::string MyServiceModuleImpl::advertise(const std::string& serviceId,\n                                              const std::string& serviceData) {\n       // discoStartAdvertising takes serviceId, serviceData and an optional\n       // base64 advertisement (a signed XPR); \"\" lets the module build it.\n       auto r = modules().libp2p_module.discoStartAdvertising(serviceId, serviceData, \"\");\n       if (!r.success) return \"advertise failed: \" + r.error;\n       return \"advertising \" + serviceId;\n   }\n\n   std::string MyServiceModuleImpl::discover(const std::string& serviceId) {\n       auto r = modules().libp2p_module.discoRegisterInterest(serviceId);\n       if (!r.success) return \"registerInterest failed: \" + r.error;\n       std::this_thread::sleep_for(std::chrono::milliseconds(500));\n       // \"\" serviceData = match any advertisement of this serviceId.\n       auto r2 = modules().libp2p_module.discoLookup(serviceId, \"\");\n       if (!r2.success) return \"lookup failed: \" + r2.error;\n       return r2.value.is_string() ? r2.value.get<std::string>() : r2.value.dump();\n   }\n\n   std::string MyServiceModuleImpl::stopDiscovery() {\n       auto r = modules().libp2p_module.discoStop();\n       if (!r.success) return \"discoStop failed: \" + r.error;\n       auto r2 = modules().libp2p_module.stop();\n       if (!r2.success) return \"stop failed: \" + r2.error;\n       return \"discovery stopped\";\n   }\n   ```\n\n1. Add `libp2p_module` as a flake input in `flake.nix`:\n\n   ```nix\n   inputs = {\n     logos-module-builder.url = \"github:logos-co/logos-module-builder\";\n     libp2p_module.url  = \"github:logos-co/logos-libp2p-module\";\n   };\n   ```\n\n   > **Info**\n   >\n   > For local development, override`flake.nix` at build time:\n   >\n   > ```bash\n   > nix build --override-input libp2p_module path:../logos-libp2p-module\n   > ```\n\n## Step 3: Build both modules\n\nThe `.#install-portable` target runs `lgpm` internally and produces the directory structure `logosctl` requires. You only write `metadata.json`; the install target generates `manifest.json`, `variant`, and co-locates all `.so` files automatically. Use `install-portable`, not `install`: `.#install` produces a development build (variant `linux-amd64-dev`) that the released `logosctl` skips with `was installed for variant 'linux-amd64-dev' which is not supported on this platform`, and `module load` then fails with `MODULE_LOAD_FAILED`.\n\n1. In `logos-my-service-module`, initialise a Git repository and run the install build:\n\n   ```sh\n   git init && git add -A\n   nix build .#install-portable -L\n   ```\n\n   This produces:\n\n   ```\n   result/modules/my_service_module/\n     ├── manifest.json\n     ├── variant\n     └── my_service_module_plugin.so\n   ```\n\n1. In `logos-libp2p-module`, run the install build:\n\n   ```sh\n   cd ../logos-libp2p-module\n   nix build .#install-portable -L\n   ```\n\n   This produces:\n\n   ```\n   result/modules/libp2p_module/\n     ├── manifest.json\n     ├── variant\n     ├── libp2p_module_plugin.so\n     ├── liblibp2p.so\n     └── …                        # bundled runtime libraries (OpenSSL, Boost, TinyCBOR)\n   ```\n\n   - `liblibp2p.so` is co-located automatically so the `$ORIGIN` RUNPATH resolves at runtime.\n\n## Step 4: Load the modules and verify the single-node flow\n\n1. Return to `logos-my-service-module` and pin a known, fixed listen address via the `LIBP2P_MODULE_CONFIG` environment variable:\n\n   ```sh\n   cd ../logos-my-service-module\n   export LIBP2P_MODULE_CONFIG='{\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"]}'\n   ```\n\n1. Start the daemon, detached so this terminal stays free, pointing at both module directories:\n\n   ```sh\n   printf 'modules_dirs:\\n  - %s\\n  - %s\\n' \\\n     \"$(cd ../logos-libp2p-module/result/modules && pwd)\" \"$(pwd)/result/modules\" \\\n     | logosctl daemon config set -\n   logosctl daemon start --detach\n   ```\n\n1. Load both modules:\n\n   ```sh\n   logosctl module load libp2p_module\n   logosctl module load my_service_module\n   ```\n\n1. Drive the discovery lifecycle and verify each call returns immediately:\n\n   ```sh\n   logosctl call my_service_module startDiscovery\n   # → discovery started\n\n   logosctl call my_service_module getPeerInfo\n   # → {\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"],\"peerId\":\"12D3KooW…\"}\n\n   logosctl call my_service_module advertise myservice/v1 version=1\n   # → advertising myservice/v1\n\n   logosctl call my_service_module discover myservice/v1\n   # → [{\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"],\"peerId\":\"12D3KooW…\",…}]   (single node: only its own advertisement)\n\n   logosctl call my_service_module stopDiscovery\n   # → discovery stopped\n   ```\n\n   - With a single node, `discover` returns only this node's own advertisement. `getPeerInfo` prints this node's `peerId` and listen address, which you need in [Step 5](#step-5-run-three-node-local-discovery) to bootstrap other instances.\n\n1. Shut down the daemon:\n\n   ```sh\n   logosctl daemon stop\n   ```\n\n## Step 5: Run three-node local discovery\n\nRun three `logosctl` daemon instances on one machine to see the Service Discovery API work: a bootstrap node, an advertiser, and a discoverer. The advertiser and discoverer are configured only with the bootstrap node as their bootstrap node—when the discoverer's lookup returns the advertiser's peer record, the advertisement provably travelled through the DHT, not over a direct A↔B link.\n\nEach daemon needs its own session (`--config-dir`) and `LIBP2P_MODULE_CONFIG` with a distinct listen port as separate sessions are what let three independent instances coexist on one machine. Run each block in a separate terminal window.\n\n1. In **Terminal 1**, start the bootstrap node:\n\n   ```sh\n   cd ../logos-my-service-module\n   export LIBP2P_MODULE_CONFIG='{\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"]}'\n   printf 'modules_dirs:\\n  - %s\\n  - %s\\n' \\\n     \"$(cd ../logos-libp2p-module/result/modules && pwd)\" \"$(pwd)/result/modules\" \\\n     | logosctl --config-dir ~/.logosctl-bootstrap daemon config set -\n   logosctl daemon start --detach --config-dir ~/.logosctl-bootstrap\n   logosctl --config-dir ~/.logosctl-bootstrap module load libp2p_module\n   logosctl --config-dir ~/.logosctl-bootstrap module load my_service_module\n   logosctl --config-dir ~/.logosctl-bootstrap call my_service_module startDiscovery\n   logosctl --config-dir ~/.logosctl-bootstrap call my_service_module getPeerInfo\n   # → note the \"peerId\" value; the bootstrap node listens on /ip4/127.0.0.1/tcp/9000\n   ```\n\n   - Copy the bootstrap node's `peerId` from the `getPeerInfo` output. Substitute it for `<BOOTSTRAP_PEER_ID>` in the next two terminals.\n\n1. In **Terminal 2**, start the advertiser:\n\n   ```sh\n   cd ../logos-my-service-module\n   export LIBP2P_MODULE_CONFIG='{\"addrs\":[\"/ip4/127.0.0.1/tcp/9001\"],\"bootstrapNodes\":[{\"peerId\":\"<BOOTSTRAP_PEER_ID>\",\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"]}]}'\n   printf 'modules_dirs:\\n  - %s\\n  - %s\\n' \\\n     \"$(cd ../logos-libp2p-module/result/modules && pwd)\" \"$(pwd)/result/modules\" \\\n     | logosctl --config-dir ~/.logosctl-advertiser daemon config set -\n   logosctl daemon start --detach --config-dir ~/.logosctl-advertiser\n   logosctl --config-dir ~/.logosctl-advertiser module load libp2p_module\n   logosctl --config-dir ~/.logosctl-advertiser module load my_service_module\n   logosctl --config-dir ~/.logosctl-advertiser call my_service_module startDiscovery\n   logosctl --config-dir ~/.logosctl-advertiser call my_service_module advertise myservice/v1 version=1\n   ```\n\n1. In **Terminal 3**, start the discoverer and look up the service:\n\n   ```sh\n   cd ../logos-my-service-module\n   export LIBP2P_MODULE_CONFIG='{\"addrs\":[\"/ip4/127.0.0.1/tcp/9002\"],\"bootstrapNodes\":[{\"peerId\":\"<BOOTSTRAP_PEER_ID>\",\"addrs\":[\"/ip4/127.0.0.1/tcp/9000\"]}]}'\n   printf 'modules_dirs:\\n  - %s\\n  - %s\\n' \\\n     \"$(cd ../logos-libp2p-module/result/modules && pwd)\" \"$(pwd)/result/modules\" \\\n     | logosctl --config-dir ~/.logosctl-discoverer daemon config set -\n   logosctl daemon start --detach --config-dir ~/.logosctl-discoverer\n   logosctl --config-dir ~/.logosctl-discoverer module load libp2p_module\n   logosctl --config-dir ~/.logosctl-discoverer module load my_service_module\n   logosctl --config-dir ~/.logosctl-discoverer call my_service_module startDiscovery\n   logosctl --config-dir ~/.logosctl-discoverer call my_service_module discover myservice/v1\n   ```\n\n   Expected output:\n\n   ```json\n   {\"method\":\"discover\",\"module\":\"my_service_module\",\n    \"result\":\"[{\\\"addrs\\\":[\\\"/ip4/127.0.0.1/tcp/9001\\\"],\\\"peerId\\\":\\\"<ADVERTISER_PEER_ID>\\\",\\\"seqNo\\\":1536,\\\"services\\\":[{\\\"data\\\":\\\"dmVyc2lvbj0x\\\",\\\"id\\\":\\\"myservice/v1\\\"}]}]\",\n    \"status\":\"ok\"}\n   ```\n\n   - The returned `peerId` is the advertiser's, and `addrs` shows its listen port (`9001`), even though the discoverer only knew about the bootstrapping node. The `services` entry carries the `serviceData` that the advertiser published, base64-encoded (`dmVyc2lvbj0x` is `version=1`).\n\n   > **Info**\n   >\n   > A first `discover` returning `[]` means the advertisement has not yet propagated. Repeat the call after a few seconds.\n\n1. Tear down all three daemons:\n\n   ```sh\n   for D in bootstrap advertiser discoverer; do\n     logosctl --config-dir ~/.logosctl-$D call my_service_module stopDiscovery\n     logosctl --config-dir ~/.logosctl-$D daemon stop\n   done\n   ```\n\n## Troubleshooting service discovery\n\n### Why does `discover` return `[]` even after waiting?\n\nThe Kad-DHT needs a few seconds to propagate the advertisement from the advertiser through the bootstrap node to the discoverer. Repeat `logosctl --config-dir ~/.logosctl-discoverer call my_service_module discover myservice/v1` after 5–10 seconds. If it still returns empty, confirm that the advertiser's `advertise` call succeeded and that both the advertiser and the discoverer share the same `<BOOTSTRAP_PEER_ID>` for the bootstrap node.\n"},{"id":"messaging/concepts/understand-peer-discovery/understand-peer-discovery","title":"Understand peer discovery","summary":"Learn how a node bootstraps and finds the peers it needs to operate.","section":"messaging","doc_type":"concept","url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-discovery","markdown_url":"https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-discovery.md","permalink":"/messaging/concepts/understand-peer-discovery/peer-discovery","source":"https://github.com/logos-co/logos-docs/blob/main/docs/messaging/concepts/understand-peer-discovery/understand-peer-discovery.md","headings":[{"depth":2,"text":"Why a node seeks more peers"},{"depth":2,"text":"Discovery mechanisms"}],"markdown":"# Understand peer discovery\n\n#### Learn how a node bootstraps and finds the peers it needs to operate.\n\nWhen initialising a [Logos Delivery](https://docs.logos.co/get-started/glossary.md#logos-delivery) node, it must connect with other peers to enable message sending, receiving, and retrieval. To achieve this, a discovery mechanism is employed to locate other peers in the network. This process is known as [bootstrapping](https://docs.logos.co/get-started/glossary.md#bootstrapping).\n\n## Why a node seeks more peers\n\nOnce a connection is established, the node must actively seek out additional peers to have:\n\n- Sufficient peers in the [Relay](https://docs.logos.co/messaging/concepts/protocols.md#relay) mesh: The goal is to have at least 6 peers in the mesh. This ensures a robust network where messages can be efficiently relayed.\n- Reserve peers for backup: It is essential to have a surplus of peers available as reserves. These reserves are backups when the current peers become overloaded or experience unexpected disconnections.\n- Peers with specific capabilities: The node seeks out peers with specific capabilities, such as [Store](https://docs.logos.co/messaging/concepts/protocols.md#store), [Light Push](https://docs.logos.co/messaging/concepts/protocols.md#light-push), or [Filter](https://docs.logos.co/messaging/concepts/protocols.md#filter). This allows for targeted interactions and enhanced functionality based on the desired capabilities.\n\n## Discovery mechanisms\n\nLogos Delivery supports multiple [peer discovery](https://docs.logos.co/get-started/glossary.md#peer-discovery) mechanisms, such as:\n\n- [Configuring Static Peers](https://docs.logos.co/messaging/concepts/understand-peer-discovery/static-peers.md)\n- [Peer Discovery via DNS](https://docs.logos.co/messaging/concepts/understand-peer-discovery/dns-discovery.md)\n- [Discv5 Ambient Peer Discovery](https://docs.logos.co/messaging/concepts/understand-peer-discovery/discv5.md)\n- [Peer Exchange](https://docs.logos.co/messaging/concepts/understand-peer-discovery/peer-exchange.md)\n"}]}