# Local regtest e2e stack: bitcoind + postgres + 3 Spark Operators + our SSP. # # Usage: # git submodule update --init --recursive # SPARK_ADMIN_TOKEN=regtest-spark-admin-token docker compose -f docker-compose.regtest.yml up --build -d # ./e2e/e2e.sh # resets a separate test project and tears it down afterward # docker compose -f docker-compose.regtest.yml down # # Sources default to the pinned Git submodules in vendor/. Override SPARK_REF # or LDK_SERVER_REF to build from other local checkouts. # # Host ports: bitcoind RPC uses BITCOIN_RPC_PORT (8332 by default), SOs use # 8535-8537, and SSP uses 5000. PostgreSQL and bitcoind ZMQ stay private. x-operator-common: &operator-common image: ${SPARK_OPERATOR_IMAGE:-open-ssp-spark-operator:regtest} build: context: ${SPARK_REF:-./vendor/spark} dockerfile: Dockerfile depends_on: postgres: condition: service_healthy bitcoin-init: condition: service_completed_successfully cert-init: condition: service_completed_successfully restart: unless-stopped entrypoint: ["/bin/sh", "-c"] volumes: - ./e2e/upstream/entrypoint.sh:/opt/spark/entrypoint.sh:ro - ./e2e/upstream/config.json:/opt/spark/config.json:ro - ./e2e/upstream/operator.config.yaml:/opt/spark/operator.config.yaml:ro - ./e2e/upstream/operator_0.key:/opt/spark/keys/operator_0.key:ro - ./e2e/upstream/operator_1.key:/opt/spark/keys/operator_1.key:ro - ./e2e/upstream/operator_2.key:/opt/spark/keys/operator_2.key:ro - tls-certs:/opt/spark/tls:ro services: cert-init: image: alpine:3.20 restart: "no" entrypoint: ["/bin/sh", "-c"] command: - | set -e apk add --no-cache openssl > /dev/null for i in 0 1 2; do KEY="/tls/server_$${i}.key" CERT="/tls/server_$${i}.crt" if [ -f "$$KEY" ] && [ -f "$$CERT" ] \ && openssl x509 -in "$$CERT" -noout -ext basicConstraints 2>/dev/null \ | grep -q "CA:FALSE"; then echo "TLS key/cert for operator $$i already exist, skipping" continue fi echo "Generating TLS server key/cert for operator $$i..." TMP_KEY="$${KEY}.tmp" TMP_CERT="$${CERT}.tmp" openssl genrsa -out "$$TMP_KEY" 2048 2>/dev/null openssl req -new -x509 \ -key "$$TMP_KEY" -out "$$TMP_CERT" \ -days 3650 \ -subj "/CN=spark-operator-$${i}" \ -addext "subjectAltName = DNS:spark-operator-$${i},DNS:localhost,DNS:spark-operator-$${i}.minikube.local" \ -addext "basicConstraints = critical,CA:FALSE" \ -addext "keyUsage = critical,digitalSignature,keyEncipherment" \ -addext "extendedKeyUsage = serverAuth" mv "$$TMP_KEY" "$$KEY" mv "$$TMP_CERT" "$$CERT" done echo "TLS certificates ready" volumes: - tls-certs:/tls postgres: image: postgres:16-bookworm environment: POSTGRES_HOST_AUTH_METHOD: trust volumes: - postgres-data:/var/lib/postgresql/data - ./e2e/upstream/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 2s timeout: 5s retries: 30 start_period: 30s bitcoind: image: lncm/bitcoind:v28.0 command: - -conf=/bitcoin.conf ports: - "127.0.0.1:${BITCOIN_RPC_PORT:-8332}:8332" volumes: - bitcoind-data:/data/.bitcoin - ./e2e/upstream/bitcoin.conf:/bitcoin.conf:ro healthcheck: test: ["CMD", "bitcoin-cli", "-regtest", "-rpcuser=testutil", "-rpcpassword=testutilpassword", "-rpcport=8332", "getblockchaininfo"] interval: 2s timeout: 5s retries: 15 bitcoin-init: image: lncm/bitcoind:v28.0 depends_on: bitcoind: condition: service_healthy restart: "no" entrypoint: ["/bin/sh", "-c"] command: - | set -e CLI="bitcoin-cli -regtest -rpcuser=testutil -rpcpassword=testutilpassword -rpcconnect=bitcoind -rpcport=8332" if ! $$CLI listwallets | grep -q '"default"'; then if $$CLI listwalletdir | grep -q '"name": "default"'; then $$CLI loadwallet default > /dev/null else $$CLI createwallet default false false '' false true > /dev/null fi fi BALANCE=$$($$CLI -rpcwallet=default getbalance) case "$$BALANCE" in 0|0.00000000) NEEDS_FUND=1 ;; *) NEEDS_FUND=0 ;; esac if [ "$$NEEDS_FUND" = "1" ]; then ADDRESS=$$($$CLI -rpcwallet=default getnewaddress) $$CLI generatetoaddress 101 "$$ADDRESS" > /dev/null echo "Funded wallet with 101 blocks" fi echo "Bitcoin wallet ready" if ! $$CLI listwallets | grep -q '"ssp-withdrawals"'; then if $$CLI listwalletdir | grep -q '"name": "ssp-withdrawals"'; then $$CLI loadwallet ssp-withdrawals > /dev/null else $$CLI createwallet ssp-withdrawals false false '' false true > /dev/null fi fi if [ "$$($$CLI -rpcwallet=ssp-withdrawals getbalance)" = "0.00000000" ]; then ADDRESS=$$($$CLI -rpcwallet=ssp-withdrawals getnewaddress '' bech32m) $$CLI -rpcwallet=default sendtoaddress "$$ADDRESS" 1 > /dev/null MINE_ADDRESS=$$($$CLI -rpcwallet=default getnewaddress) $$CLI generatetoaddress 3 "$$MINE_ADDRESS" > /dev/null fi bitcoin-miner: image: lncm/bitcoind:v28.0 depends_on: bitcoin-init: condition: service_completed_successfully restart: unless-stopped entrypoint: ["/bin/sh", "-c"] command: - | CLI="bitcoin-cli -regtest -rpcuser=testutil -rpcpassword=testutilpassword -rpcconnect=bitcoind -rpcport=8332" while true; do ADDRESS=$$($$CLI -rpcwallet=default getnewaddress 2>&1) || { sleep 10; continue; } $$CLI generatetoaddress 1 "$$ADDRESS" > /dev/null 2>&1 && \ echo "$$(date): Mined 1 block" || \ echo "$$(date): ERROR: Failed to mine block" sleep 10 done electrs: image: coffernetwork/regtest-electrs:28.1@sha256:7cd973541afc28e75ff18b93050df4e91749b38af031930ed2b77f4088a3d246 depends_on: bitcoind: condition: service_healthy command: - --network=regtest - --jsonrpc-import - --cookie=testutil:testutilpassword - --daemon-rpc-addr=bitcoind:8332 - --db-dir=/electrs - --electrum-rpc-addr=0.0.0.0:60401 - --http-addr=0.0.0.0:3002 - --monitoring-addr=0.0.0.0:24224 - --cors=* - --address-search ports: - "127.0.0.1:30000:3002" volumes: - electrs-data:/electrs restart: unless-stopped spark-operator-0: <<: *operator-common command: - /opt/spark/entrypoint.sh 0 ports: - "127.0.0.1:8535:8535" expose: - "8536" networks: default: aliases: - spark-operator-0.minikube.local spark-operator-1: <<: *operator-common command: - /opt/spark/entrypoint.sh 1 ports: - "127.0.0.1:8536:8535" expose: - "8536" networks: default: aliases: - spark-operator-1.minikube.local spark-operator-2: <<: *operator-common command: - /opt/spark/entrypoint.sh 2 ports: - "127.0.0.1:8537:8535" expose: - "8536" networks: default: aliases: - spark-operator-2.minikube.local # ---- Lightning backend --------------------------------------------------- # Built from a local ldk-server checkout (set LDK_SERVER_REF). The SSP talks # to it over gRPC whenever LDK_GRPC_ADDR and credentials are set. ldk-server: image: ${LDK_SERVER_IMAGE:-open-ssp-ldk-server:regtest} build: context: ${LDK_SERVER_REF:-./vendor/ldk-server} dockerfile: Dockerfile depends_on: bitcoind: condition: service_healthy ports: - "127.0.0.1:3536:3536" - "127.0.0.1:19735:9735" healthcheck: test: ["CMD-SHELL", "test -r /data/regtest/macaroons/admin.macaroon && ldk-server-cli --base-url localhost:3536 --macaroon $$(tr -d '[:space:]' < /data/regtest/macaroons/admin.macaroon) --tls-cert /data/tls.crt get-node-info >/dev/null"] interval: 10s timeout: 5s retries: 12 volumes: - ldk-data:/data - ./e2e/ldk-server-config.toml:/config.toml:ro entrypoint: ["/bin/sh", "-c"] command: - | ldk-server /config.toml & PID=$$! trap 'kill -TERM "$$PID"' TERM INT while kill -0 "$$PID" 2>/dev/null; do if [ -f /data/regtest/macaroons/admin.macaroon ] && [ -f /data/tls.crt ]; then chmod 0755 /data/regtest /data/regtest/macaroons chmod 0444 /data/regtest/macaroons/admin.macaroon /data/tls.crt break fi sleep 1 done wait "$$PID" restart: unless-stopped # Second node: LN counterparty for e2e send/receive (channel peer for ldk-server). ldk-server-2: image: ${LDK_SERVER_IMAGE:-open-ssp-ldk-server:regtest} build: context: ${LDK_SERVER_REF:-./vendor/ldk-server} dockerfile: Dockerfile depends_on: bitcoind: condition: service_healthy ports: - "127.0.0.1:3537:3536" - "127.0.0.1:19736:9735" volumes: - ldk-data-2:/data - ./e2e/ldk-server-2-config.toml:/config.toml:ro entrypoint: ["/bin/sh", "-c"] command: - | ldk-server /config.toml & PID=$$! trap 'kill -TERM "$$PID"' TERM INT while kill -0 "$$PID" 2>/dev/null; do if [ -f /data/regtest/macaroons/admin.macaroon ] && [ -f /data/tls.crt ]; then chmod 0755 /data/regtest /data/regtest/macaroons chmod 0444 /data/regtest/macaroons/admin.macaroon /data/tls.crt break fi sleep 1 done wait "$$PID" restart: unless-stopped healthcheck: test: ["CMD-SHELL", "test -r /data/regtest/macaroons/admin.macaroon && ldk-server-cli --base-url localhost:3536 --macaroon $$(tr -d '[:space:]' < /data/regtest/macaroons/admin.macaroon) --tls-cert /data/tls.crt get-node-info >/dev/null"] interval: 10s timeout: 5s retries: 12 ssp: # Prebuilt: docker compose pull (needs GHCR access); or --build locally. image: ${SSP_IMAGE:-ghcr.io/benthecarman/open-ssp:latest} build: context: . dockerfile: ssp.Dockerfile depends_on: spark-operator-0: condition: service_started spark-operator-1: condition: service_started spark-operator-2: condition: service_started ldk-server: condition: service_healthy ports: - "127.0.0.1:5000:5000" extra_hosts: - "host.docker.internal:host-gateway" healthcheck: test: ["CMD", "open-ssp", "healthcheck"] interval: 10s timeout: 5s retries: 12 volumes: - ssp-data:/data - ldk-data:/ldk-data:ro - tls-certs:/tls:ro environment: SSP_LISTEN_ADDR: 0.0.0.0:5000 SSP_CORS_ORIGINS: ${SSP_CORS_ORIGINS:-} SSP_NETWORK: ${SSP_NETWORK:-REGTEST} SSP_DATA_DIR: /data SSP_PUBLIC_URL: http://ssp:5000 SSP_WEBHOOK_ALLOW_LOCAL: ${SSP_WEBHOOK_ALLOW_LOCAL:-0} SSP_INSTANT_MAX_OUTSTANDING_SATS: ${SSP_INSTANT_MAX_OUTSTANDING_SATS:-0} SSP_INSTANT_MAX_DEPOSIT_SATS: ${SSP_INSTANT_MAX_DEPOSIT_SATS:-0} COOP_BITCOIN_RPC_URL: http://bitcoind:8332/wallet/ssp-withdrawals COOP_BITCOIN_RPC_USER: testutil COOP_BITCOIN_RPC_PASSWORD: testutilpassword SPARK_MNEMONIC_FILE: /data/spark.mnemonic SO_HOSTS: ${SO_HOSTS:-spark-operator-0.minikube.local:8535,spark-operator-1.minikube.local:8535,spark-operator-2.minikube.local:8535} SO_IDENTITY_PUBKEYS: ${SO_IDENTITY_PUBKEYS:-0322ca18fc489ae25418a0e768273c2c61cabb823edfb14feb891e9bec62016510,0341727a6c41b168f07eb50865ab8c397a53c7eef628ac1020956b705e43b6cb27,0305ab8d485cc752394de4981f8a5ae004f2becfea6f432c9a59d5022d8764f0a6} SO_CERT_FILES: /tls/server_0.crt,/tls/server_1.crt,/tls/server_2.crt SSP_OPERATOR_HOSTS: spark-operator-0.minikube.local:8536,spark-operator-1.minikube.local:8536,spark-operator-2.minikube.local:8536 SSP_OPERATOR_CERT_FILES: /tls/server_0.crt,/tls/server_1.crt,/tls/server_2.crt # Regtest withdrawal fees can require leaves smaller than 330 sats. SSP_MIN_SPLIT_CHILD_SATS: 1 SPARK_ADMIN_TOKEN: ${SPARK_ADMIN_TOKEN:?set SPARK_ADMIN_TOKEN} # Live LDK backend: bitcoind-backed ldk-server above. LDK_GRPC_ADDR: ${LDK_GRPC_ADDR:-ldk-server:3536} LDK_MACAROON: ${LDK_MACAROON:-} LDK_MACAROON_FILE: ${LDK_MACAROON_FILE:-/ldk-data/regtest/macaroons/admin.macaroon} LDK_TLS_CERT_FILE: ${LDK_TLS_CERT_FILE:-/ldk-data/tls.crt} SSP_SWAP_FEE_SATS: 0 SSP_FROST_THRESHOLD: 2 RUST_LOG: info # The second SSP gives each Breez wallet a distinct Lightning node. This # prevents the test from paying an invoice back to the same node. ssp-2: image: ${SSP_IMAGE:-ghcr.io/benthecarman/open-ssp:latest} build: context: . dockerfile: ssp.Dockerfile depends_on: spark-operator-0: condition: service_started spark-operator-1: condition: service_started spark-operator-2: condition: service_started ldk-server-2: condition: service_healthy ports: - "127.0.0.1:5001:5000" extra_hosts: - "host.docker.internal:host-gateway" healthcheck: test: ["CMD", "open-ssp", "healthcheck"] interval: 10s timeout: 5s retries: 12 volumes: - ssp-data-2:/data - ldk-data-2:/ldk-data:ro - tls-certs:/tls:ro environment: SSP_LISTEN_ADDR: 0.0.0.0:5000 SSP_NETWORK: REGTEST SSP_DATA_DIR: /data SSP_PUBLIC_URL: http://ssp-2:5000 SSP_WEBHOOK_ALLOW_LOCAL: ${SSP_WEBHOOK_ALLOW_LOCAL:-0} SSP_INSTANT_MAX_OUTSTANDING_SATS: ${SSP_INSTANT_MAX_OUTSTANDING_SATS:-0} SSP_INSTANT_MAX_DEPOSIT_SATS: ${SSP_INSTANT_MAX_DEPOSIT_SATS:-0} SPARK_MNEMONIC_FILE: /data/spark.mnemonic SO_HOSTS: spark-operator-0.minikube.local:8535,spark-operator-1.minikube.local:8535,spark-operator-2.minikube.local:8535 SO_IDENTITY_PUBKEYS: 0322ca18fc489ae25418a0e768273c2c61cabb823edfb14feb891e9bec62016510,0341727a6c41b168f07eb50865ab8c397a53c7eef628ac1020956b705e43b6cb27,0305ab8d485cc752394de4981f8a5ae004f2becfea6f432c9a59d5022d8764f0a6 SO_CERT_FILES: /tls/server_0.crt,/tls/server_1.crt,/tls/server_2.crt SSP_OPERATOR_HOSTS: spark-operator-0.minikube.local:8536,spark-operator-1.minikube.local:8536,spark-operator-2.minikube.local:8536 SSP_OPERATOR_CERT_FILES: /tls/server_0.crt,/tls/server_1.crt,/tls/server_2.crt SSP_MIN_SPLIT_CHILD_SATS: 330 SPARK_ADMIN_TOKEN: ${SPARK_ADMIN_TOKEN:?set SPARK_ADMIN_TOKEN} LDK_GRPC_ADDR: ldk-server-2:3536 LDK_MACAROON_FILE: /ldk-data/regtest/macaroons/admin.macaroon LDK_TLS_CERT_FILE: /ldk-data/tls.crt SSP_SWAP_FEE_SATS: 0 SSP_FROST_THRESHOLD: 2 RUST_LOG: info volumes: postgres-data: bitcoind-data: electrs-data: tls-certs: ldk-data: ldk-data-2: ssp-data: ssp-data-2: