Register an RLN membership on LEZ with a Delivery node
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.
This document is accurate for Testnet v0.3.0.
This procedure shows how to register an Rate-Limiting Nullifier (RLN) membership on the 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, and LEZ in a single flow.
- A supported OS:
- macOS (Apple Silicon)
- Linux (x86-64 or aarch64)
logosctlinstalled.- Install it by running
curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh
- Install it by running
curlandpython3- Network access to
github.comfor the catalogue downloads, and to the testnet sequencer athttps://testnet.lez.logos.co/ - About 1 GB of free disk space. The modules take about 300 MB.
250000000units of native LEZ tokens for the node's payer account, from one of these sources:- A Logos Blockchain wallet you can bridge assets from. Testnet has no faucet.
- A LEZ account you already hold on the testnet zone, with about
270000000units so it can cover the transfer's fee reserve.
What to expect
- You can run a
delivery_modulenode onlogos.testwith an active RLN membership registered on LEZ. - You can fund the node's payer account and watch the node register its membership by itself.
- You can send a message that carries an RLN proof and confirm that it propagated and used up part of your epoch quota.
Step 1: Set up a working directory
Run every command in this procedure in one shell, from one working directory.
-
Create a working directory and change into it:
mkdir -p ~/logos-rln && cd ~/logos-rln -
Set the session directory. Every
logosctlcommand in this shell uses it to hold the daemon's state, its logs, and the installed modules:export LOGOSCTL_CONFIG_DIR=$PWD/session
Step 2: Start the daemon with a wallet home
The 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.
-
Create an empty wallet home:
mkdir -p wallet-home -
Start the daemon. It passes its environment to the modules, which is how the wallet finds its home:
LEE_WALLET_HOME_DIR=$PWD/wallet-home \NSSA_WALLET_HOME_DIR=$PWD/wallet-home \logosctl daemon start --detachExpected result:
Daemon started (pid N)and the path of its log,session/logs/daemon.log. The command returns once the daemon is ready. -
Check the daemon's status:
logosctl daemon status --jsonExpected result:
"daemon":{"status":"running",...}, withcapability_module,package_manager, andpackage_downloaderloaded.
Step 3: Install and load the modules
-
Install the delivery module and the RLN module from the official catalogue.
liblogos_rln_modulepulls inliblogos_lez_rln_module:logosctl package install delivery_module liblogos_rln_module -y -
List the installed packages:
logosctl package lsExpected result:
delivery_module0.3.3 or later,liblogos_rln_module0.12.0 or later, andliblogos_lez_rln_module5.1.0 or later, each with sourceuser.- Older delivery modules point
logos.testat 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.
- Older delivery modules point
-
Load the delivery module. This also loads the RLN modules, which are its optional dependencies:
logosctl module load delivery_modulelogosctl module ls --loadedExpected result:
liblogos_lez_rln_module,liblogos_rln_module, anddelivery_moduleare loaded.
Step 4: Create the delivery node
-
Capture the node's events in the background. The watcher only sees events emitted after it attaches, so start it first:
logosctl --json watch delivery_module >> events.jsonl 2>&1 & -
Write the node config and create the node:
cat > node.json <<'EOF'{"mode": "Core", "preset": "logos.test"}EOFlogosctl --json call delivery_module createNode @$PWD/node.jsonField Purpose modeCoreruns a full relay node.presetlogos.testbrings 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.messagingOverridesOptional 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.Expected result:
"success":true. Creating the node also starts the RLN module on the registry that the preset names. The LEZ RLN module writeswallet-home/wallet_config.json, pointing at the testnet sequencer, andwallet-home/network.json, containing{"network":"testnet","source":"table"}. From here on, the node registers itself once its payer account is funded. -
Poll the RLN state until it reports
"state":"Ready":logosctl --json call delivery_module rlnStateExpected result: the reply names the RLN scope the node uses:
"registryId":"logos:testnet:<64 hex>", "rlnIdentifier":"<64 hex>", "epochSizeSec":<N> -
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 thatlogosctlcan't mistake it for a number:REGISTRY=<registryId>printf '%s' '<rlnIdentifier>' > rln-id.arg
Step 5: Fund the node's payer account
The 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.
-
Poll the wallet status until
stateisready:logosctl --json call liblogos_lez_rln_module wallet_statusExpected result:
{"status":"ok","result":{... "state":"ready", "network":"testnet", "payer":"<ACCOUNT>" ...}}. While the wallet syncs,stateispending. -
Convert the payer account ID to the
Public/<base58>form that LEZ wallets and explorers use. Replace<ACCOUNT>with the 64-character hex value ofpayer, which is also the recipient a bridge deposit carries:ACCOUNT=<ACCOUNT>python3 -c 'import sysA = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"h = sys.argv[1]n, s = int(h, 16), ""while n:n, r = divmod(n, 58)s = A[r] + sprint("Public/" + "1" * ((len(h) - len(h.lstrip("0"))) // 2) + s)' $ACCOUNT -
Send
250000000native units to the payer account:- To bridge from the Logos Blockchain, follow Bridge assets between the blockchain and a zone. 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.
- To transfer from an existing LEZ account, send to the
Public/<base58>address. A transfer carries a fee reserve of about19000000on top of the amount, at the minimum fee for the wallet's standard 2M gas limit; all but the actual fee (about100000) is refunded to the sender.
-
Check the payer's balance. An empty argument file means the node's own payer:
: > self.arglogosctl --json call liblogos_lez_rln_module get_native_balance @$PWD/self.argExpected result:
balanceis at or above the amount you sent.
Step 6: Wait for the membership to register
The node registers itself on the LEZ registry, typically one to three minutes after the funding lands.
-
Poll the membership state until
stateisactive:logosctl --json call liblogos_rln_module get_membership_state $REGISTRY @$PWD/rln-id.arg- While registering, the reply shows
"state":"unknown"and"provisioning":{"step":"awaiting_funding"}or"step":"registering". - Once the registration is submitted, the reply shows
"state":"pending"with aleaf_index, until the transaction is in a LEZ block.
Expected result:
"state":"active"(or"grace_period"), with aleaf_indexand amembership_hash."state":"failed"means registration failed; see Troubleshooting. Ifdaemon.logsaysno membership within 300s, the module only stopped waiting: check that the sequencer is producing blocks withgetLastBlockId, and keep polling. - While registering, the reply shows
Step 7: Start the node and send a message
The node only starts with an active membership.
-
Start the node and check that it reported
nodeStarted:logosctl --json call delivery_module startuntil grep nodeStarted events.jsonl; do sleep 1; done -
Wait about 15 seconds for the node to join the Relay mesh, then subscribe to a content topic:
sleep 15TOPIC=/my-app/1/hello/protologosctl --json call delivery_module subscribe $TOPIC -
Note the epoch quota before sending. The
str:prefix passes the timestamp as a string:logosctl --json call liblogos_rln_module get_epoch_quota $REGISTRY @$PWD/rln-id.arg str:$(date +%s)Expected result:
{"epoch_index":N,"rate_limit":N,"remaining":N}. -
Send a message:
printf '%s' "hello from my node" > payload.arglogosctl --json call delivery_module send $TOPIC @$PWD/payload.argExpected result:
"success":true, with arequestIdas the value. -
Check that the node generated a proof and the message propagated:
grep -iE 'rlnGenerateProofRequest|messagePropagated|messageError' events.jsonlExpected result: within about 30 seconds, a proof-generation request for the send (
rlnGenerateProofRequestordispatchRlnGenerateProofRequestEvent), then:{"event":"messagePropagated","data":{"arg0":"<requestId>","arg1":"<message hash>"}}- A
messageErrorevent with yourrequestIdmeans the send failed; its data carries the reason.
- A
-
Run the
get_epoch_quotacall again.Expected result: in the same
epoch_index,remainingis lower than before the send. -
Stop the daemon when you're finished:
logosctl daemon stop- The wallet, membership, and installed packages stay in
wallet-home/andsession/. To run the node again, start the daemon with the same environment as in Step 2, thenmodule load delivery_module, start the event watcher, runcreateNodewith the samenode.json, andstartoncerlnStateisReady. The existing membership is picked up; nothing is registered or paid again.
- The wallet, membership, and installed packages stay in
Troubleshooting RLN membership
A string argument arrives as a number
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.
createNode fails with an unknown preset
Spell the preset exactly logos.test. logostest is rejected, not treated as an alias.
rlnState stays Disabled
The 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.
rlnState reports Failed
The 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.
Another 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.
wallet_status reports failed, or daemon.log shows Unexpected variant tag
The 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/.
wallet_status stays pending with the network selected
The 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:
curl -sS -X POST -H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"getLastBlockId","params":[],"id":1}' \
https://testnet.lez.logos.co/
wallet_status stays pending with no network selected
Nothing has named a registry yet. Run createNode with the logos.test preset as in Step 4. If it persists, the installed RLN modules are too old; see Step 3.
The wallet never becomes ready, or opens in the wrong place
The 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.
Registry calls fail with bound to network
wallet-home/ was used for another network before. A wallet home bound to one network refuses another. Use a fresh wallet home per network.
provisioning.step stays at awaiting_funding
The node waits indefinitely for its payer balance. Check get_native_balance and complete the funding transfer.
Registration fails with Incorrect fee
The payer holds less than the price plus the fee reserve (182800000 in total at the minimum fee). Top it up to 250000000.
Registration never finishes and daemon.log contains outside registry bounds
The registry rejected the requested rate limit. The log line names the allowed range.
get_epoch_quota returns rate_limit: 0
The node has no usable membership yet; the quota is not exhausted. Wait for get_membership_state to report active.
send fails with no_usable_membership
Registration has not finished. Wait for get_membership_state to report active.
Receivers drop the first message after registration
A newly published tree root takes up to one root window to reach other nodes. Later sends are unaffected.
daemon start fails, or daemon status reports not_running
Read session/daemon/startup.err and session/logs/daemon.log.
Every command fails with no daemon
The 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.
package install does nothing and asks for confirmation
Without a terminal, package install refuses to proceed unless given -y.
package install can't find liblogos_rln_module
The 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.
After a restart the membership reads unknown and sends fail
A 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.