mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
parent
80af65d7fe
commit
b790dc7abf
6 changed files with 186 additions and 14 deletions
|
|
@ -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 <http://localhost:3000> — password `rtldev`. All three nodes appear in
|
||||
the node switcher.
|
||||
Then open <http://localhost:3000> — 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.
|
||||
|
|
|
|||
35
docker/cln/poststart.d/create-rune.sh
Executable file
35
docker/cln/poststart.d/create-rune.sh
Executable file
|
|
@ -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="<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}"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue