diff --git a/docker/.env b/docker/.env index ed45187c..0ecf69d6 100644 --- a/docker/.env +++ b/docker/.env @@ -1,18 +1,29 @@ -BITCOIN_HOST=bitcoind -BITCOIN_PORT=18889 -BITCOIN_RPC_USER=bitcoin -BITCOIN_RPC_PASSWORD=bitcoin -BITCOIN_RPC_PORT=18888 -BITCOIN_ZMQ_TX_PORT=28888 -BITCOIN_ZMQ_BLOCK_PORT=28889 - -LIGHTNING_HOST=lnd -LIGHTNING_PORT=9735 -LIGHTNING_RPC_PORT=10009 -LIGHTNING_REST_PORT=8080 -LIGHTNING_LOOP_PORT=8081 - -RTL_PORT=3000 +# Regtest dev fixture. NOT for production. Credentials here are throwaway. COMPOSE_FILE=docker-compose.yml COMPOSE_PROJECT_NAME=rtldev + +# bitcoind. The rpcauth hash for these credentials is baked into docker-compose.yml; +# if you change the user/password here you must regenerate it (see README). +BITCOIN_HOST=bitcoind +BITCOIN_RPC_USER=rtldev +BITCOIN_RPC_PASSWORD=rtldev +BITCOIN_RPC_PORT=18443 +BITCOIN_P2P_PORT=18444 +BITCOIN_ZMQ_BLOCK_PORT=28334 +BITCOIN_ZMQ_TX_PORT=28335 + +# LND. Ports are the container-internal ones (identical for every node); +# host-side mappings are assigned per node in docker-compose.yml. +LIGHTNING_REST_PORT=8080 +LIGHTNING_RPC_PORT=10009 +LIGHTNING_P2P_PORT=9735 + +# Host-side LND REST ports, one per node +ALICE_REST_PORT=8081 +BOB_REST_PORT=8082 +CAROL_REST_PORT=8083 + +# RTL +RTL_PORT=3000 +RTL_PASSWORD=password diff --git a/docker/README.md b/docker/README.md index 0e3134d6..48533f9e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,96 +1,115 @@ -# 1) RTL Docker Dev Setup +# RTL regtest dev fixture -### This is not suitable for production deployments. ONLY FOR DEVELOPMENT. +### NOT suitable for production. Development only. Every credential here is throwaway. -This `docker-compose` template launches `bitcoind`, `lnd` and `rtl` containers. - -It is configured to run in **regtest** mode but can be modified to suit your needs. - -### 1.1) Notes - - `bitcoind` is built from an Ubuntu repository and should not be used in production. - - `lnd` will not sync to chain until Bitcoin regtest blocks are generated (see below). - - `rtl` image is from the Docker Hub repository but you can change this to your needs. - - Various ports and configs can be adjusted in the `.env` or `docker-compose.yml` files. - -## 1.2) How to run -It may take several minutes if containers need to be built. - -1.2.1) From the terminal in this folder: +A self-contained regtest network for developing and testing RTL: `bitcoind`, three +LND nodes, and RTL wired to all three. ``` -$ docker-compose up -d bitcoind -$ bin/b-cli generate 101 -$ docker-compose up -d lnd rtl +alice --[ 5,000,000 sat ]--> bob --[ 3,000,000 sat ]--> carol ``` -1.2.2) Check containers are up and running with: -``` -$ docker-compose ps +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. + +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. + +## Requirements + +Docker with Compose v2 (`docker compose`, not `docker-compose`). + +## Quick start + +From this directory: + +```bash +docker compose up -d # bitcoind, alice, bob, carol, rtl +./scripts/seed.sh # fund, connect, open channels, make payments ``` -1.2.3) Use the cli tools to get responses from the containers: -``` -$ bin/ln-cli getinfo -$ bin/b-cli getblockchaininfo +Then open — password `password`. All three nodes appear in +the node switcher. + +Tear down, discarding all state: + +```bash +docker compose down -v ``` -1.2.4) View daemon logs as follows: -``` -$ docker-compose logs bitcoind lnd rtl +## What the seed creates + +| | | +|---|---| +| 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) | +| 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 | + +## Determinism + +Every amount and payment in `scripts/seed.sh` is fixed. A fresh run always produces +identical state, so screenshots taken before and after a change differ only by the +change. **Do not introduce randomness.** + +The seed is deterministic but deliberately *not* idempotent — running it twice would +fund every node again and open a second set of channels. It refuses to run against an +already-seeded network. To start over: + +```bash +docker compose down -v && docker compose up -d && ./scripts/seed.sh ``` -Once the containers are running you can access the RTL UI at http://localhost:3000 +## Helpers - - Default password is `password`. - - Default host, port and password can be changed in `.env`. - -When you are done you can destroy containers with: -``` -$ docker-compose down -v -``` ---- -# 2) Stand alone RTL Setup -This is suitable when you already have a LND node running and configured. - -## 2.1) From docker image pull -``` -RTL_VERSION=0.12.0 -docker run --name rtl -d -it \ --e RTL_CONFIG_PATH=/RTLConfig \ --v /path/to/RTLConfig/dir:/RTLConfig \ --v /path/to/macaroon/dir:/path/as/specified/in/RTLConfig \ --v /path/to/database/dir:/RTL/database \ --p 3000:3000/tcp \ -shahanafarooqui/rtl:${RTL_VERSION} +```bash +bin/b-cli getblockcount # bitcoin-cli +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 ``` -## 2.2) From local docker build -### 2.2.1) Build the image locally -``` -RTL_VERSION=0.12.0 -docker build -t rtl:${RTL_VERSION} -f dockerfiles/Dockerfile . -``` -### 2.2.2) Create .env file -Create an environment file with your required configurations. Sample .env: -``` -RTL_CONFIG_PATH=/RTLConfig -LN_IMPLEMENTATION=LND -MACAROON_PATH=/LNDMacaroon -LN_SERVER_URL=https://host.docker.internal:8080 +Logs: +```bash +docker compose logs -f rtl +docker compose logs alice ``` -### 2.2.3) Run the newly built image with .env configurations -``` -RTL_VERSION=0.12.0 -docker run -d -it \ --v /path/to/RTLConfig/dir:/RTLConfig \ --v /path/to/macaroon/dir:/LNDMacaroon \ --v /path/to/database/dir:/RTL/database \ ---env-file=.env -p 3000:3000 rtl:${RTL_VERSION} +## Notes and gotchas + +**RTL's config.** `rtl/RTL-Config.regtest.json` is the tracked template. RTL rewrites +its config on startup, so an init container copies it into a volume rather than +bind-mounting it — a read-only mount makes RTL exit with `EROFS`, and a writable one +would let RTL modify a version-controlled file. The name is not `RTL-Config.json` +because `.gitignore` matches that bare filename at any depth. + +**`lncli` needs `--lnddir=/home/lnd/.lnd`.** `docker compose exec` lands as root, +whose HOME is `/root`, but lnd's datadir is `/home/lnd/.lnd`. `bin/ln-cli` handles this. + +**Changing bitcoind credentials.** `docker-compose.yml` carries an `-rpcauth` hash for +the `BITCOIN_RPC_USER` / `BITCOIN_RPC_PASSWORD` in `.env`. Changing them there is not +enough; regenerate the hash: + +```bash +python3 - <<'EOF' +import hmac, hashlib +user, password, salt = "rtldev", "rtldev", "8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e" +print(f"{user}:{salt}${hmac.new(salt.encode(), password.encode(), hashlib.sha256).hexdigest()}") +EOF ``` -Once the container is running you can access the RTL UI at http://localhost:3000 +In `docker-compose.yml` the `$` must be written `$$` to escape Compose interpolation. ---- -@hashamadeus on Twitter +**Payments right after channel open will fail.** The channel graph has to reach alice +before she can route to carol. The seed waits for this; anything you script yourself +should too. + +## 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. diff --git a/docker/bin/b-cli b/docker/bin/b-cli index d43ba4d4..55d1002c 100755 --- a/docker/bin/b-cli +++ b/docker/bin/b-cli @@ -1,10 +1,20 @@ #!/usr/bin/env bash +# +# bitcoin-cli against the regtest fixture. +# +# bin/b-cli getblockchaininfo +# bin/b-cli -rpcwallet=rtldev getbalance +# bin/b-cli -rpcwallet=rtldev generatetoaddress 6
+set -euo pipefail + +cd "$(dirname "$0")/.." +# shellcheck disable=SC1091 source .env -docker-compose exec bitcoind bitcoin-cli \ - -datadir=/bitcoin \ - -rpcuser=$BITCOIN_RPC_USER \ - -rpcpassword=$BITCOIN_RPC_PASSWORD \ - -rpcport=$BITCOIN_RPC_PORT \ - "$@" \ No newline at end of file +exec docker compose exec -T bitcoind bitcoin-cli \ + -regtest \ + -rpcuser="$BITCOIN_RPC_USER" \ + -rpcpassword="$BITCOIN_RPC_PASSWORD" \ + -rpcport="$BITCOIN_RPC_PORT" \ + "$@" diff --git a/docker/bin/ln-cli b/docker/bin/ln-cli index 0ef12af1..e87c213c 100755 --- a/docker/bin/ln-cli +++ b/docker/bin/ln-cli @@ -1,8 +1,32 @@ #!/usr/bin/env bash +# +# lncli against one node of the regtest fixture. The node name is required, +# because the fixture runs three of them. +# +# bin/ln-cli alice getinfo +# bin/ln-cli bob listchannels +# bin/ln-cli carol addinvoice --amt=1000 +# +# --lnddir is passed explicitly: 'docker compose exec' lands as root, whose HOME +# is /root, but lnd's datadir is /home/lnd/.lnd. Without it lncli looks for the +# TLS cert in the wrong place and fails. -source .env +set -euo pipefail -docker-compose exec lnd lncli \ - --macaroonpath /shared/admin.macaroon \ - --tlscertpath /shared/tls.cert \ - "$@" \ No newline at end of file +cd "$(dirname "$0")/.." + +node="${1:-}" +case "$node" in + alice|bob|carol) + shift + ;; + *) + echo "usage: $(basename "$0") [lncli args...]" >&2 + exit 1 + ;; +esac + +exec docker compose exec -T "$node" lncli \ + --network=regtest \ + --lnddir=/home/lnd/.lnd \ + "$@" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8daed008..d7af2def 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,132 +1,183 @@ -version: "2.4" +# Regtest dev fixture for RTL: bitcoind + 3 LND nodes + RTL. +# +# NOT suitable for production. All credentials are throwaway. +# +# Topology is alice -> bob -> carol, so bob forwards payments and RTL's +# routing/forwarding screens have data in them. See README.md. +# +# Node images come from Polar (https://lightningpolar.com), which publishes +# multi-arch (amd64 + arm64) builds. Nothing is built locally. volumes: - bitcoin_data: - lightning_data: - lightning_shared: + bitcoind_data: + alice_data: + bob_data: + carol_data: rtl_db: + rtl_config: + +x-lnd: &lnd + image: polarlightning/lnd:0.20.0-beta + restart: unless-stopped + depends_on: + - bitcoind services: bitcoind: container_name: ${COMPOSE_PROJECT_NAME}_bitcoind - image: bitcoind:0.19.0 - build: ./bitcoind - command: [ - "bitcoind", - "-datadir=/bitcoin", - "-port=${BITCOIN_PORT}", - "-upnp=0", - "-dnsseed=0", - "-txindex=1", - "-listen=0", - "-onlynet=ipv4", - "-regtest=1", - "-regtest.rpcport=${BITCOIN_RPC_PORT}", - "-regtest.port=${BITCOIN_PORT}", - "-rpcport=${BITCOIN_RPC_PORT}", - "-rpcuser=${BITCOIN_RPC_USER}", - "-rpcpassword=${BITCOIN_RPC_PASSWORD}", - "-rpcallowip=0.0.0.0/0", - "-zmqpubrawtx=tcp://0.0.0.0:${BITCOIN_ZMQ_TX_PORT}", - "-zmqpubrawblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT}", - "-zmqpubhashblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT}" - ] - ports: - - "${BITCOIN_PORT}:${BITCOIN_PORT}" - volumes: - - bitcoin_data:/bitcoin - - lnd: - container_name: ${COMPOSE_PROJECT_NAME}_lnd - image: lnd:0.12.0-beta - build: ./lnd + image: polarlightning/bitcoind:30.0 restart: unless-stopped - command: [ - "lnd", - "--noseedbackup", - "--rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}", - "--restlisten=0.0.0.0:${LIGHTNING_REST_PORT}", - "--adminmacaroonpath=/shared/admin.macaroon", - "--tlsextradomain=${LIGHTNING_HOST}", - "--tlsextraip=0.0.0.0", - "--tlscertpath=/shared/tls.cert", - "--datadir=/lnd", - "--bitcoin.active", - "--bitcoin.regtest", - "--bitcoin.node=bitcoind", - "--bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}", - "--bitcoind.rpcuser=${BITCOIN_RPC_USER}", - "--bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}", - "--bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}", - "--bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}" - ] - depends_on: + command: - bitcoind + - -server=1 + - -regtest=1 + # rpcauth hash for ${BITCOIN_RPC_USER}/${BITCOIN_RPC_PASSWORD}. '$$' escapes + # compose interpolation and reaches bitcoind as a single '$'. + - -rpcauth=rtldev:8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e$$010df4b32c5e9a556cba1857eb5865990c983d8a56dadc0fdbf457cf90073c6c + - -zmqpubrawblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT} + - -zmqpubrawtx=tcp://0.0.0.0:${BITCOIN_ZMQ_TX_PORT} + - -txindex=1 + - -dnsseed=0 + - -rpcbind=0.0.0.0 + - -rpcallowip=0.0.0.0/0 + - -rpcport=${BITCOIN_RPC_PORT} + - -listen=1 + - -listenonion=0 + - -fallbackfee=0.0002 ports: - - "${LIGHTNING_REST_PORT}:${LIGHTNING_REST_PORT}" + - "${BITCOIN_RPC_PORT}:${BITCOIN_RPC_PORT}" volumes: - - lightning_data:/lnd - - lightning_shared:/shared + - bitcoind_data:/home/bitcoin/.bitcoin - boltz: - container_name: ${COMPOSE_PROJECT_NAME}_boltz - image: boltz:1.2.0 - build: ./boltz - restart: unless-stopped - command: [ - "boltz", - "--noseedbackup", - "--rpclisten=0.0.0.0:${BOLTZ_RPC_PORT}", - "--restlisten=0.0.0.0:${BOLTZ_REST_PORT}", - "--adminmacaroonpath=/shared/admin.macaroon", - "--tlsextradomain=${BOLTZ_HOST}", - "--tlsextraip=0.0.0.0", - "--tlscertpath=/shared/tls.cert", - "--datadir=/boltz", - "--bitcoin.active", - "--bitcoin.regtest", - "--bitcoin.node=bitcoind", - "--bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}", - "--bitcoind.rpcuser=${BITCOIN_RPC_USER}", - "--bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}", - "--bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}", - "--bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}" - ] - depends_on: - - bitcoind + # --alias / --externalip / --tlsextradomain are per-node on purpose: the alias + # is what RTL displays, and the extradomain must match the hostname RTL dials + # (https://alice:8080) or TLS validation fails. + alice: + <<: *lnd + container_name: ${COMPOSE_PROJECT_NAME}_alice + command: + - lnd + - --noseedbackup + - --trickledelay=5000 + - --alias=alice + - --externalip=alice + - --tlsextradomain=alice + - --tlsextradomain=${COMPOSE_PROJECT_NAME}_alice + - --tlsextradomain=host.docker.internal + - --listen=0.0.0.0:${LIGHTNING_P2P_PORT} + - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT} + - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT} + - --bitcoin.active + - --bitcoin.regtest + - --bitcoin.node=bitcoind + - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT} + - --bitcoind.rpcuser=${BITCOIN_RPC_USER} + - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD} + - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT} + - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT} + - --accept-keysend + - --accept-amp ports: - - "${BOLTZ_REST_PORT}:${BOLTZ_REST_PORT}" + - "${ALICE_REST_PORT}:${LIGHTNING_REST_PORT}" volumes: - - boltz_data:/boltz - - boltz_shared:/shared - + - alice_data:/home/lnd/.lnd + + bob: + <<: *lnd + container_name: ${COMPOSE_PROJECT_NAME}_bob + command: + - lnd + - --noseedbackup + - --trickledelay=5000 + - --alias=bob + - --externalip=bob + - --tlsextradomain=bob + - --tlsextradomain=${COMPOSE_PROJECT_NAME}_bob + - --tlsextradomain=host.docker.internal + - --listen=0.0.0.0:${LIGHTNING_P2P_PORT} + - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT} + - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT} + - --bitcoin.active + - --bitcoin.regtest + - --bitcoin.node=bitcoind + - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT} + - --bitcoind.rpcuser=${BITCOIN_RPC_USER} + - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD} + - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT} + - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT} + - --accept-keysend + - --accept-amp + ports: + - "${BOB_REST_PORT}:${LIGHTNING_REST_PORT}" + volumes: + - bob_data:/home/lnd/.lnd + + carol: + <<: *lnd + container_name: ${COMPOSE_PROJECT_NAME}_carol + command: + - lnd + - --noseedbackup + - --trickledelay=5000 + - --alias=carol + - --externalip=carol + - --tlsextradomain=carol + - --tlsextradomain=${COMPOSE_PROJECT_NAME}_carol + - --tlsextradomain=host.docker.internal + - --listen=0.0.0.0:${LIGHTNING_P2P_PORT} + - --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT} + - --restlisten=0.0.0.0:${LIGHTNING_REST_PORT} + - --bitcoin.active + - --bitcoin.regtest + - --bitcoin.node=bitcoind + - --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT} + - --bitcoind.rpcuser=${BITCOIN_RPC_USER} + - --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD} + - --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT} + - --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT} + - --accept-keysend + - --accept-amp + ports: + - "${CAROL_REST_PORT}:${LIGHTNING_REST_PORT}" + volumes: + - carol_data:/home/lnd/.lnd + + # 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 + # template is copied into a volume that 'down -v' discards, which keeps the + # source pristine and every run starting from identical config. + rtl-config-init: + container_name: ${COMPOSE_PROJECT_NAME}_rtl_config_init + image: busybox:1.36 + command: > + sh -c "cp /template/RTL-Config.regtest.json /config/RTL-Config.json && + chmod 644 /config/RTL-Config.json && + echo 'config staged'" + volumes: + - ./rtl/RTL-Config.regtest.json:/template/RTL-Config.regtest.json:ro + - rtl_config:/config + rtl: container_name: ${COMPOSE_PROJECT_NAME}_rtl - image: shahanafarooqui/rtl:0.12.0 + image: shahanafarooqui/rtl:v0.15.8 restart: unless-stopped depends_on: - - lnd - volumes: - - lightning_shared:/shared:ro - - rtl_db:/database + rtl-config-init: + condition: service_completed_successfully + alice: + condition: service_started + bob: + condition: service_started + carol: + condition: service_started ports: - "${RTL_PORT}:${RTL_PORT}" environment: - PORT: ${RTL_PORT} - HOST: 192.168.0.27 - MACAROON_PATH: /shared - LN_SERVER_URL: https://${LIGHTNING_HOST}:${LIGHTNING_REST_PORT} - CONFIG_PATH: '' - LN_IMPLEMENTATION: LND - SWAP_SERVER_URL: https://${LIGHTNING_HOST}:${LIGHTNING_LOOP_PORT} - SWAP_MACAROON_PATH: /shared - BOLTZ_SERVER_URL: https://${BOLTZ_HOST}:${BOLTZ_PORT} - BOLTZ_MACAROON_PATH: /shared - RTL_SSO: 0 - RTL_COOKIE_PATH: '' - LOGOUT_REDIRECT_LINK: '' - RTL_CONFIG_PATH: /RTL - BITCOIND_CONFIG_PATH: '' - CHANNEL_BACKUP_PATH: /shared/lnd/backup - ENABLE_OFFERS: false - ENABLE_PEERSWAP: false + RTL_CONFIG_PATH: /RTL/config + volumes: + - rtl_config:/RTL/config + - alice_data:/lnd/alice:ro + - bob_data:/lnd/bob:ro + - carol_data:/lnd/carol:ro + - rtl_db:/RTL/database diff --git a/docker/rtl/RTL-Config.regtest.json b/docker/rtl/RTL-Config.regtest.json new file mode 100644 index 00000000..0be5d6f7 --- /dev/null +++ b/docker/rtl/RTL-Config.regtest.json @@ -0,0 +1,67 @@ +{ + "multiPass": "password", + "port": "3000", + "defaultNodeIndex": 1, + "dbDirectoryPath": "/RTL/database", + "SSO": { + "rtlSSO": 0, + "rtlCookiePath": "", + "logoutRedirectLink": "" + }, + "nodes": [ + { + "index": 1, + "lnNode": "alice", + "lnImplementation": "LND", + "authentication": { + "macaroonPath": "/lnd/alice/data/chain/bitcoin/regtest" + }, + "settings": { + "userPersona": "OPERATOR", + "themeMode": "DAY", + "themeColor": "PURPLE", + "logLevel": "ERROR", + "lnServerUrl": "https://alice:8080", + "fiatConversion": false, + "unannouncedChannels": false, + "blockExplorerUrl": "https://mempool.space" + } + }, + { + "index": 2, + "lnNode": "bob", + "lnImplementation": "LND", + "authentication": { + "macaroonPath": "/lnd/bob/data/chain/bitcoin/regtest" + }, + "settings": { + "userPersona": "OPERATOR", + "themeMode": "DAY", + "themeColor": "PURPLE", + "logLevel": "ERROR", + "lnServerUrl": "https://bob:8080", + "fiatConversion": false, + "unannouncedChannels": false, + "blockExplorerUrl": "https://mempool.space" + } + }, + { + "index": 3, + "lnNode": "carol", + "lnImplementation": "LND", + "authentication": { + "macaroonPath": "/lnd/carol/data/chain/bitcoin/regtest" + }, + "settings": { + "userPersona": "MERCHANT", + "themeMode": "DAY", + "themeColor": "PURPLE", + "logLevel": "ERROR", + "lnServerUrl": "https://carol:8080", + "fiatConversion": false, + "unannouncedChannels": false, + "blockExplorerUrl": "https://mempool.space" + } + } + ] +} diff --git a/docker/scripts/seed.sh b/docker/scripts/seed.sh new file mode 100755 index 00000000..7134cd7f --- /dev/null +++ b/docker/scripts/seed.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# +# Seed the regtest fixture with a deterministic scenario. +# +# Every amount, capacity and payment below is fixed on purpose. Re-running this +# against a fresh network must produce the same state, so that screenshots taken +# now and after a redesign differ only by the design. Do not introduce randomness. +# +# Topology: +# +# alice --[ 5,000,000 sat ]--> bob --[ 3,000,000 sat ]--> carol +# +# bob sits in the middle so it accrues forwarding history, which is what +# populates RTL's routing screens. +# +# Usage: ./scripts/seed.sh (from the docker/ directory) + +set -euo pipefail + +cd "$(dirname "$0")/.." + +BITCOIN_RPC_USER="${BITCOIN_RPC_USER:-rtldev}" +BITCOIN_RPC_PASSWORD="${BITCOIN_RPC_PASSWORD:-rtldev}" + +NODES=(alice bob carol) + +# Deterministic scenario constants +FUND_SATS=10000000 # on-chain funding per node +CH_ALICE_BOB=5000000 # channel capacity alice -> bob +CH_BOB_CAROL=3000000 # channel capacity bob -> carol +PUSH_SATS=1000000 # pushed to remote on open, so both sides have liquidity +MINE_CONFIRM=6 # blocks to confirm a funding tx + +log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; } +info() { printf ' %s\n' "$*"; } +die() { printf '\n\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } + +bcli() { + docker compose exec -T bitcoind bitcoin-cli -regtest \ + -rpcuser="$BITCOIN_RPC_USER" -rpcpassword="$BITCOIN_RPC_PASSWORD" "$@" +} + +# 'docker compose exec' lands as root, whose HOME is /root, but lnd's datadir is +# /home/lnd/.lnd -- so lncli must be told where to find the cert and macaroon. +lncli() { + local node=$1; shift + docker compose exec -T "$node" lncli --network=regtest --lnddir=/home/lnd/.lnd "$@" +} + +# 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. +json_first() { + grep -o "\"$1\": *\"[^\"]*\"" | head -1 | sed -e 's/^[^:]*: *"//' -e 's/"$//' +} + +# Wait for a command to succeed, up to N attempts. +wait_for() { + local desc=$1 attempts=$2; shift 2 + local i=1 + while (( i <= attempts )); do + if "$@" >/dev/null 2>&1; then + info "$desc ready (${i}s)" + return 0 + fi + sleep 1 + (( i++ )) + done + die "timed out after ${attempts}s waiting for: $desc" +} + +# ---------------------------------------------------------------- bitcoind + +log "Waiting for bitcoind" +wait_for "bitcoind RPC" 60 bcli getblockchaininfo + +log "Preparing wallet" +if ! bcli listwallets | grep -q '"rtldev"'; then + bcli createwallet rtldev >/dev/null 2>&1 || bcli loadwallet rtldev >/dev/null +fi +info "wallet rtldev present" + +MINE_ADDR=$(bcli -rpcwallet=rtldev getnewaddress) +info "mining address: $MINE_ADDR" + +HEIGHT=$(bcli getblockcount) +if (( HEIGHT < 101 )); then + log "Mining 101 blocks (coinbase maturity)" + bcli -rpcwallet=rtldev generatetoaddress 101 "$MINE_ADDR" >/dev/null +else + info "chain already at height $HEIGHT, skipping initial mine" +fi + +# ---------------------------------------------------------------- lnd nodes + +log "Waiting for LND nodes" +for n in "${NODES[@]}"; do + wait_for "$n" 120 lncli "$n" getinfo +done + +# This script is deterministic, not idempotent: running it twice would fund every +# node again and open a second set of channels. Refuse rather than corrupt the +# fixture, since the whole point is that a fresh run reproduces identical state. +if lncli alice listchannels | grep -q '"chan_id"'; then + die "network is already seeded -- re-running would double-fund it. + Reset with: docker compose down -v && docker compose up -d && ./scripts/seed.sh" +fi + +log "Funding nodes (${FUND_SATS} sats each)" +BTC_AMOUNT=$(awk "BEGIN{printf \"%.8f\", $FUND_SATS/100000000}") +for n in "${NODES[@]}"; do + addr=$(lncli "$n" newaddress p2wkh | json_first address) + [ -n "$addr" ] || die "could not get address for $n" + bcli -rpcwallet=rtldev sendtoaddress "$addr" "$BTC_AMOUNT" >/dev/null + info "$n <- $BTC_AMOUNT BTC ($addr)" +done + +bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null +info "mined $MINE_CONFIRM blocks to confirm funding" + +log "Waiting for confirmed on-chain balances" +for n in "${NODES[@]}"; do + for i in $(seq 1 60); do + bal=$(lncli "$n" walletbalance | json_first confirmed_balance) + bal=${bal:-0} + (( bal > 0 )) && { info "$n confirmed balance: $bal sats"; break; } + sleep 1 + (( i == 60 )) && die "$n never saw confirmed funds" + done +done + +# ---------------------------------------------------------------- peers + +pubkey_of() { + lncli "$1" getinfo | json_first identity_pubkey +} + +log "Connecting peers" +BOB_PUB=$(pubkey_of bob) +CAROL_PUB=$(pubkey_of carol) +info "bob pubkey: $BOB_PUB" +info "carol pubkey: $CAROL_PUB" + +lncli alice connect "${BOB_PUB}@bob:9735" >/dev/null 2>&1 || info "alice->bob already connected" +lncli bob connect "${CAROL_PUB}@carol:9735" >/dev/null 2>&1 || info "bob->carol already connected" +info "peers connected" + +# ---------------------------------------------------------------- channels + +log "Opening channels" +lncli alice openchannel --node_key="$BOB_PUB" \ + --local_amt="$CH_ALICE_BOB" --push_amt="$PUSH_SATS" >/dev/null +info "alice -> bob ${CH_ALICE_BOB} sats (push ${PUSH_SATS})" + +lncli bob openchannel --node_key="$CAROL_PUB" \ + --local_amt="$CH_BOB_CAROL" --push_amt="$PUSH_SATS" >/dev/null +info "bob -> carol ${CH_BOB_CAROL} sats (push ${PUSH_SATS})" + +bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null +info "mined $MINE_CONFIRM blocks to confirm channels" + +log "Waiting for channels to become active" +for n in alice bob; do + for i in $(seq 1 60); do + active=$(lncli "$n" listchannels | grep -c '"active": *true' || true) + (( active > 0 )) && { info "$n has $active active channel(s)"; break; } + sleep 1 + (( i == 60 )) && die "$n has no active channels" + done +done + +# ---------------------------------------------------------------- payments + +# alice can only route to carol once the bob->carol channel has been announced and +# reached her graph. Channels are confirmed by now, but gossip is not instant -- +# --trickledelay alone is 5s. Paying before this lands fails with "no route". +log "Waiting for the channel graph to reach alice" +for i in $(seq 1 90); do + edges=$(lncli alice describegraph | grep -c '"channel_id"' || true) + (( ${edges:-0} >= 2 )) && { info "alice sees ${edges} channels in her graph"; break; } + sleep 1 + (( i == 90 )) && die "channel graph never propagated to alice" +done + +# Fixed amounts. alice -> carol routes through bob, generating forwarding history. +log "Sending payments (alice -> carol, routed via bob)" +for amt in 10000 25000 50000 75000 100000; do + inv=$(lncli carol addinvoice --amt="$amt" --memo="seed payment ${amt} sats" \ + | json_first payment_request) + if lncli alice payinvoice --force --pay_req="$inv" >/dev/null 2>&1; then + info "alice -> carol ${amt} sats (routed)" + else + info "alice -> carol ${amt} sats FAILED (route not ready?)" + fi +done + +log "Sending direct payments (alice -> bob)" +for amt in 5000 15000; do + inv=$(lncli bob addinvoice --amt="$amt" --memo="direct payment ${amt} sats" \ + | json_first payment_request) + lncli alice payinvoice --force --pay_req="$inv" >/dev/null 2>&1 \ + && info "alice -> bob ${amt} sats" \ + || info "alice -> bob ${amt} sats FAILED" +done + +# Unsettled invoices, so the invoice list shows more than one state. +log "Creating open (unpaid) invoices on carol" +for amt in 20000 40000; do + lncli carol addinvoice --amt="$amt" --memo="open invoice ${amt} sats" >/dev/null + info "carol open invoice ${amt} sats" +done + +# ---------------------------------------------------------------- summary + +log "Seed complete" +for n in "${NODES[@]}"; do + chans=$(lncli "$n" listchannels | grep -c '"active": *true' || true) + bal=$(lncli "$n" walletbalance | json_first confirmed_balance) + printf ' %-6s channels: %-3s on-chain: %s sats\n' "$n" "${chans:-0}" "${bal:-0}" +done +# '|| 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) +printf ' bob forwarded %s payment(s)\n' "${fwds:-0}" +printf '\n RTL: http://localhost:%s (password: %s)\n\n' "${RTL_PORT:-3000}" "${RTL_PASSWORD:-password}"