mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL
The setup in docker/ has been unable to start since Feb 2021. The boltz
service added in f817ae39 references BOLTZ_* variables that are not in .env
(so its ports render as ":" and compose rejects the file with "invalid
proto"), a ./boltz build context that has never existed in the repo, and
boltz_data/boltz_shared volumes that are never declared. Because compose
validates the whole project up front, this broke every service: "docker
compose up -d bitcoind", the first command in the README, failed too.
Rather than repair a five-year-old file pinned to bitcoind 0.19.0 and lnd
0.12.0-beta, this replaces it.
What changed:
- bitcoind 30.0 and 3x lnd 0.20.0-beta, using Polar's images. They are
multi-arch, so nothing is built locally and this works on arm64. The old
setup built bitcoind and lnd from local Dockerfiles.
- Three nodes, not one: alice -> bob -> carol. bob forwards, so RTL's
routing and forwarding screens have data. Two nodes leave them empty.
- scripts/seed.sh funds the nodes, opens channels, and makes payments with
fixed amounts. A fresh run reproduces identical state, so screenshots
taken before and after a change differ only by the change. It is
deliberately not idempotent and refuses to run against a seeded network,
since re-running would double-fund it.
- rtl/RTL-Config.regtest.json configures all three nodes. RTL rewrites its
config on startup, so an init container stages a copy into a volume: a
read-only bind mount makes RTL exit with EROFS, and a writable one would
let RTL modify a tracked file. It is not named RTL-Config.json because
.gitignore matches that bare name at any depth.
- bin/ln-cli now takes a node name and passes --lnddir=/home/lnd/.lnd,
because 'docker compose exec' lands as root while lnd's datadir is under
/home/lnd. Both helpers use compose v2.
- README rewritten to match.
Boltz, Core Lightning and Eclair are left out of this pass. Polar publishes
multi-arch clightning and eclair images, so adding them later needs compose
services, config entries and seed adapters, but no image building.
Verified from a clean 'down -v': all nodes sync, channels go active, 5/5
payments route through bob, bob records 5 forwards, and RTL serves the UI
with all three nodes configured. Two independent from-scratch runs produced
identical balances.
The old bitcoind/ and lnd/ build contexts are now unreferenced but left in
place for a follow-up.
This commit is contained in:
parent
da25d16fb4
commit
4e2b7ca978
7 changed files with 619 additions and 212 deletions
41
docker/.env
41
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
|
||||
|
|
|
|||
169
docker/README.md
169
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 <http://localhost:3000> — 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.
|
||||
|
|
|
|||
|
|
@ -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 <address>
|
||||
|
||||
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 \
|
||||
"$@"
|
||||
exec docker compose exec -T bitcoind bitcoin-cli \
|
||||
-regtest \
|
||||
-rpcuser="$BITCOIN_RPC_USER" \
|
||||
-rpcpassword="$BITCOIN_RPC_PASSWORD" \
|
||||
-rpcport="$BITCOIN_RPC_PORT" \
|
||||
"$@"
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
"$@"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
node="${1:-}"
|
||||
case "$node" in
|
||||
alice|bob|carol)
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "usage: $(basename "$0") <alice|bob|carol> [lncli args...]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exec docker compose exec -T "$node" lncli \
|
||||
--network=regtest \
|
||||
--lnddir=/home/lnd/.lnd \
|
||||
"$@"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
67
docker/rtl/RTL-Config.regtest.json
Normal file
67
docker/rtl/RTL-Config.regtest.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
225
docker/scripts/seed.sh
Executable file
225
docker/scripts/seed.sh
Executable file
|
|
@ -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}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue