From b790dc7abfdb967b2132300959dfb80912e2999a Mon Sep 17 00:00:00 2001 From: saubyk <39208279+saubyk@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:03:34 -0700 Subject: [PATCH] Add a Core Lightning node to the regtest docker fixture The fixture had only bitcoind + three LND nodes, so RTL's Core Lightning screens had no backend to exercise. Add a `cln` node (official elementsproject/lightningd image, multi-arch) wired to RTL over clnrest with rune auth, and have the seed open a cln->alice channel so the CLN channel/peer screens have real data. - docker-compose.yml: cln service (clnrest on 0.0.0.0:3010, https), a healthcheck gated on the rune file so rtl waits for it, and rtl now mounts the cln volume read-only and depends on cln being healthy. The rtl image is parameterized via ${RTL_IMAGE:-...} so an unreleased build can be tested against the fixture. - cln/poststart.d/create-rune.sh: once the RPC is up, create a master rune and write it as LIGHTNING_RUNE="..." where RTL reads it (runePath). Polls for RPC readiness because the image entrypoint can invoke poststart before the socket exists. - RTL-Config.regtest.json: add node index 4 (CLN, rune auth, https://cln:3010). - seed.sh: fund cln, connect to alice, open a 4,000,000 sat channel, wait active. - README + release notes updated. Used to verify the CLN channel connection-status fix (#1606) end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker/README.md | 40 +++++++++++++++------- docker/cln/poststart.d/create-rune.sh | 35 +++++++++++++++++++ docker/docker-compose.yml | 49 ++++++++++++++++++++++++++- docker/rtl/RTL-Config.regtest.json | 18 ++++++++++ docker/scripts/seed.sh | 48 ++++++++++++++++++++++++++ release-notes/Release-notes-0.15.9.md | 10 ++++++ 6 files changed, 186 insertions(+), 14 deletions(-) create mode 100755 docker/cln/poststart.d/create-rune.sh diff --git a/docker/README.md b/docker/README.md index 3f35c750..ccc2b6a9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -3,17 +3,22 @@ ### NOT suitable for production. Development only. Every credential here is throwaway. A self-contained regtest network for developing and testing RTL: `bitcoind`, three -LND nodes, and RTL wired to all three. +LND nodes, a Core Lightning node, and RTL wired to all four. ``` alice --[ 5,000,000 sat ]--> bob --[ 3,000,000 sat ]--> carol +cln --[ 4,000,000 sat ]--> alice ``` bob sits in the middle so it accrues forwarding history, which is what gives RTL's -routing screens something to show. Two nodes would leave them empty. +routing screens something to show. Two nodes would leave them empty. The `cln` +(Core Lightning) node gives RTL's CLN screens a real backend — it talks to RTL over +clnrest with rune auth. -Node images come from [Polar](https://lightningpolar.com), which publishes multi-arch -(amd64 + arm64) builds. Nothing is built locally, so this works on Apple Silicon. +LND and bitcoind images come from [Polar](https://lightningpolar.com); the Core +Lightning image is the official [`elementsproject/lightningd`](https://hub.docker.com/r/elementsproject/lightningd). +All are multi-arch (amd64 + arm64) and nothing is built locally, so this works on +Apple Silicon. ## Requirements @@ -24,12 +29,12 @@ Docker with Compose v2 (`docker compose`, not `docker-compose`). From this directory: ```bash -docker compose up -d # bitcoind, alice, bob, carol, rtl +docker compose up -d # bitcoind, alice, bob, carol, cln, rtl ./scripts/seed.sh # fund, connect, open channels, make payments ``` -Then open — password `rtldev`. All three nodes appear in -the node switcher. +Then open — password `rtldev`. All four nodes (alice, bob, +carol, cln) appear in the node switcher. Tear down, discarding all state: @@ -41,12 +46,12 @@ docker compose down -v | | | |---|---| -| On-chain | 10,000,000 sats per node, confirmed | -| Channels | alice→bob 5,000,000 sats · bob→carol 3,000,000 sats (1,000,000 pushed each) | +| On-chain | 10,000,000 sats per node (LND) + 10,000,000 sats on cln, confirmed | +| Channels | alice→bob 5,000,000 sats · bob→carol 3,000,000 sats (1,000,000 pushed each) · cln→alice 4,000,000 sats | | Routed payments | 5 × alice→carol via bob (10k, 25k, 50k, 75k, 100k sats) | | Direct payments | 2 × alice→bob (5k, 15k sats) | | Open invoices | 2 unpaid on carol (20k, 40k sats) | -| Personas | alice + bob OPERATOR, carol MERCHANT | +| Personas | alice + bob + cln OPERATOR, carol MERCHANT | ## Determinism @@ -70,6 +75,7 @@ bin/b-cli -rpcwallet=rtldev getbalance bin/ln-cli alice getinfo # lncli, node name required bin/ln-cli bob listchannels bin/ln-cli bob fwdinghistory # forwarding history +docker compose exec cln lightning-cli --network=regtest listpeerchannels # Core Lightning ``` Logs: @@ -108,8 +114,16 @@ In `docker-compose.yml` the `$` must be written `$$` to escape Compose interpola before she can route to carol. The seed waits for this; anything you script yourself should too. +**Core Lightning auth uses a rune.** RTL talks to `cln` over clnrest and authenticates +with a rune, not a macaroon. On first start `cln/poststart.d/create-rune.sh` runs inside +the node (once the RPC is up), creates a master rune, and writes it as +`LIGHTNING_RUNE="…"` to `rtl.rune` in the shared `cln_data` volume; RTL reads it via the +`runePath` in its config. The `cln` healthcheck only passes once that file exists, so RTL +waits for it. `--clnrest-host=0.0.0.0` is required for RTL (another container) to reach +clnrest; the default `127.0.0.1` would only be reachable from inside the node. + ## Not included -Core Lightning and Eclair nodes, and the Boltz swap service. Polar publishes -multi-arch `clightning` and `eclair` images, so adding them means compose services, -RTL config entries, and seeding adapters — no image building. +Eclair nodes and the Boltz swap service. Polar publishes a multi-arch `eclair` image, so +adding an Eclair node means a compose service, an RTL config entry, and a seeding adapter +— no image building. diff --git a/docker/cln/poststart.d/create-rune.sh b/docker/cln/poststart.d/create-rune.sh new file mode 100755 index 00000000..45a4108b --- /dev/null +++ b/docker/cln/poststart.d/create-rune.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# Runs from Core Lightning's lightning-poststart.d. Creates a master rune (once) +# and stores it in the format RTL expects: a file containing +# LIGHTNING_RUNE="" +# which RTL reads via its runePath. +# +# The image entrypoint can invoke poststart scripts before the RPC socket is +# ready (it watches the datadir with a race that loses on a fresh node), so poll +# for `getinfo` before calling createrune. Idempotent: keeps the same rune across +# restarts so RTL's stored auth stays valid. + +RUNE_FILE="${LIGHTNINGD_DATA}/rtl.rune" + +[ -f "${RUNE_FILE}" ] && exit 0 + +# Wait (up to ~120s) for the RPC to accept commands. +for _ in $(seq 1 120); do + if lightning-cli --network="${LIGHTNINGD_NETWORK}" getinfo >/dev/null 2>&1; then + break + fi + sleep 1 +done + +rune=$(lightning-cli --network="${LIGHTNINGD_NETWORK}" createrune 2>/dev/null \ + | grep -o '"rune"[[:space:]]*:[[:space:]]*"[^"]*"' \ + | sed -e 's/.*"rune"[[:space:]]*:[[:space:]]*"//' -e 's/"$//') + +if [ -z "${rune}" ]; then + echo "create-rune.sh: failed to create rune" >&2 + exit 1 +fi + +printf 'LIGHTNING_RUNE="%s"\n' "${rune}" > "${RUNE_FILE}" +echo "create-rune.sh: wrote rune to ${RUNE_FILE}" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d7af2def..2fe456c7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,6 +13,7 @@ volumes: alice_data: bob_data: carol_data: + cln_data: rtl_db: rtl_config: @@ -142,6 +143,47 @@ services: volumes: - carol_data:/home/lnd/.lnd + # Core Lightning node. Unlike the LND nodes it talks to RTL over clnrest (the + # built-in REST plugin) using rune auth, so it needs --clnrest-* options and a + # rune written where RTL can read it. The entrypoint runs scripts dropped in + # lightning-poststart.d once the RPC socket is up; create-rune.sh generates the + # rune there and writes it to /root/.lightning/rtl.rune (RTL mounts that read-only). + # --clnrest-host=0.0.0.0 is required so the rtl container can reach it; the default + # 127.0.0.1 would only be reachable from inside this container. Protocol stays https + # (clnrest default, self-signed) — RTL connects with rejectUnauthorized:false. + cln: + image: elementsproject/lightningd:v25.09 + container_name: ${COMPOSE_PROJECT_NAME}_cln + restart: unless-stopped + depends_on: + - bitcoind + environment: + LIGHTNINGD_NETWORK: regtest + command: + - --alias=cln + - --bitcoin-rpcconnect=${BITCOIN_HOST} + - --bitcoin-rpcport=${BITCOIN_RPC_PORT} + - --bitcoin-rpcuser=${BITCOIN_RPC_USER} + - --bitcoin-rpcpassword=${BITCOIN_RPC_PASSWORD} + - --bitcoin-retry-timeout=3600 + - --addr=0.0.0.0:9735 + - --announce-addr=cln:9735 + - --large-channels + - --clnrest-host=0.0.0.0 + - --clnrest-port=3010 + ports: + - "${CLN_REST_PORT:-3010}:3010" + volumes: + - cln_data:/root/.lightning + - ./cln/poststart.d:/root/.lightning/lightning-poststart.d:ro + healthcheck: + # Healthy only once the RPC is up AND the rune file has been written, so the + # rtl service (which reads the rune at startup) can wait on this. + test: ["CMD-SHELL", "lightning-cli --network=regtest getinfo >/dev/null 2>&1 && test -f /root/.lightning/rtl.rune"] + interval: 5s + timeout: 5s + retries: 40 + # RTL rewrites its config file on startup, so it cannot be given the tracked # template directly: a bind mount would either be read-only (RTL exits with # EROFS) or would let RTL scribble into a version-controlled file. Instead the @@ -160,7 +202,9 @@ services: rtl: container_name: ${COMPOSE_PROJECT_NAME}_rtl - image: shahanafarooqui/rtl:v0.15.8 + # Defaults to the published image; override with RTL_IMAGE (e.g. a locally built + # branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up. + image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.8} restart: unless-stopped depends_on: rtl-config-init: @@ -171,6 +215,8 @@ services: condition: service_started carol: condition: service_started + cln: + condition: service_healthy ports: - "${RTL_PORT}:${RTL_PORT}" environment: @@ -180,4 +226,5 @@ services: - alice_data:/lnd/alice:ro - bob_data:/lnd/bob:ro - carol_data:/lnd/carol:ro + - cln_data:/cln:ro - rtl_db:/RTL/database diff --git a/docker/rtl/RTL-Config.regtest.json b/docker/rtl/RTL-Config.regtest.json index 927280a4..70233d85 100644 --- a/docker/rtl/RTL-Config.regtest.json +++ b/docker/rtl/RTL-Config.regtest.json @@ -62,6 +62,24 @@ "unannouncedChannels": false, "blockExplorerUrl": "https://mempool.space" } + }, + { + "index": 4, + "lnNode": "cln", + "lnImplementation": "CLN", + "authentication": { + "runePath": "/cln/rtl.rune" + }, + "settings": { + "userPersona": "OPERATOR", + "themeMode": "DAY", + "themeColor": "PURPLE", + "logLevel": "ERROR", + "lnServerUrl": "https://cln:3010", + "fiatConversion": false, + "unannouncedChannels": false, + "blockExplorerUrl": "https://mempool.space" + } } ] } diff --git a/docker/scripts/seed.sh b/docker/scripts/seed.sh index e0495d57..e503bc65 100755 --- a/docker/scripts/seed.sh +++ b/docker/scripts/seed.sh @@ -38,6 +38,7 @@ NODES=(alice bob carol) FUND_SATS=10000000 # on-chain funding per node CH_ALICE_BOB=5000000 # channel capacity alice -> bob CH_BOB_CAROL=3000000 # channel capacity bob -> carol +CH_CLN_ALICE=4000000 # channel capacity cln -> alice (Core Lightning node) PUSH_SATS=1000000 # pushed to remote on open, so both sides have liquidity MINE_CONFIRM=6 # blocks to confirm a funding tx @@ -57,6 +58,11 @@ lncli() { docker compose exec -T "$node" lncli --network=regtest --lnddir=/home/lnd/.lnd "$@" } +# Core Lightning cli. Runs inside the cln container against the regtest node. +clncli() { + docker compose exec -T cln lightning-cli --network=regtest "$@" +} + # Extract the first value for a JSON key from lncli output. # 'first' matters: walletbalance reports confirmed_balance at the top level AND # again under account_balance.default, and lncli emits no --json flag we can use. @@ -146,8 +152,10 @@ pubkey_of() { } log "Connecting peers" +ALICE_PUB=$(pubkey_of alice) BOB_PUB=$(pubkey_of bob) CAROL_PUB=$(pubkey_of carol) +info "alice pubkey: $ALICE_PUB" info "bob pubkey: $BOB_PUB" info "carol pubkey: $CAROL_PUB" @@ -220,6 +228,44 @@ for amt in 20000 40000; do info "carol open invoice ${amt} sats" done +# ---------------------------------------------------------------- core lightning + +# A Core Lightning node with one active channel, so RTL's CLN screens have data. +# An active (peer_connected) channel is what exercises the connection-status column. +log "Waiting for Core Lightning node" +wait_for "cln" 120 clncli getinfo + +log "Funding Core Lightning (${FUND_SATS} sats)" +CLN_ADDR=$(clncli newaddr | json_first bech32) +[ -n "$CLN_ADDR" ] || die "could not get address for cln" +bcli -rpcwallet=rtldev sendtoaddress "$CLN_ADDR" "$BTC_AMOUNT" >/dev/null +info "cln <- $BTC_AMOUNT BTC ($CLN_ADDR)" +bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null +info "mined $MINE_CONFIRM blocks to confirm cln funding" + +log "Waiting for cln confirmed on-chain balance" +for i in $(seq 1 60); do + clncli listfunds | grep -q '"status": "confirmed"' && { info "cln funds confirmed"; break; } + sleep 1 + (( i == 60 )) && die "cln never saw confirmed funds" +done + +log "Opening channel cln -> alice" +clncli connect "${ALICE_PUB}@alice:9735" >/dev/null 2>&1 || info "cln->alice already connected" +clncli fundchannel "$ALICE_PUB" "$CH_CLN_ALICE" >/dev/null +info "cln -> alice ${CH_CLN_ALICE} sats" +bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null +info "mined $MINE_CONFIRM blocks to confirm the cln channel" + +log "Waiting for the cln channel to become active" +for i in $(seq 1 90); do + if clncli listpeerchannels | grep -o '"state": "[A-Z_]*"' | grep -q "CHANNELD_NORMAL"; then + info "cln channel is CHANNELD_NORMAL"; break + fi + sleep 1 + (( i == 90 )) && die "cln channel never reached CHANNELD_NORMAL" +done + # ---------------------------------------------------------------- summary log "Seed complete" @@ -228,6 +274,8 @@ for n in "${NODES[@]}"; do bal=$(lncli "$n" walletbalance | json_first confirmed_balance) printf ' %-6s channels: %-3s on-chain: %s sats\n' "$n" "${chans:-0}" "${bal:-0}" done +cln_chans=$(clncli listpeerchannels | grep -o '"state": "[A-Z_]*"' | grep -c "CHANNELD_NORMAL" || true) +printf ' %-6s channels: %-3s (Core Lightning)\n' "cln" "${cln_chans:-0}" # '|| echo 0' would be wrong here: grep -c already prints 0 when it finds nothing # and then exits 1, so the echo would append a second line. fwds=$(lncli bob fwdinghistory | grep -c '"chan_id_in"' || true) diff --git a/release-notes/Release-notes-0.15.9.md b/release-notes/Release-notes-0.15.9.md index 1387be67..6a72fbb4 100644 --- a/release-notes/Release-notes-0.15.9.md +++ b/release-notes/Release-notes-0.15.9.md @@ -15,3 +15,13 @@ this release should add its entry under the appropriate section below. `connected = peer_connected` in the `listPeerChannels` response so legacy consumers stay in sync, and the list columns read `peer_connected` directly. Regression tests were added for both channel tables. + +## Developer Tooling + +- **Added a Core Lightning node to the regtest docker fixture** + ([#1625](https://github.com/Ride-The-Lightning/RTL/pull/1625)). + The `docker/` fixture now runs a `cln` node (official `elementsproject/lightningd` image) + alongside the three LND nodes, wired to RTL over clnrest with rune auth, and the seed opens + a `cln→alice` channel. This gives RTL's Core Lightning screens a real backend for local + development and testing — it was used to verify the CLN channel-connection fix above + end-to-end. See `docker/README.md`.