From 88b5ebfa3cf35aa61b7905214f500b8c0901d9c5 Mon Sep 17 00:00:00 2001 From: alexpaynex <55271826-alexpaynex@users.noreply.replit.com> Date: Wed, 18 Mar 2026 18:13:29 +0000 Subject: [PATCH] Set up Bitcoin node and Lightning infrastructure with Docker Create Docker Compose configuration, Bitcoin and LND configuration files, and bootstrap/init scripts for setting up a Bitcoin full node, LND, and LNbits on a Digital Ocean droplet. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 0b0f1422-94e9-40dc-9b10-29c5f33a1bac Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8 Replit-Helium-Checkpoint-Created: true --- infrastructure/configs/bitcoin.conf | 30 +++++ infrastructure/configs/lnd.conf | 32 +++++ infrastructure/docker-compose.yml | 87 ++++++++++++++ infrastructure/lnd-init.sh | 156 +++++++++++++++++++++++++ infrastructure/ops.sh | 128 ++++++++++++++++++++ infrastructure/setup.sh | 173 ++++++++++++++++++++++++++++ 6 files changed, 606 insertions(+) create mode 100644 infrastructure/configs/bitcoin.conf create mode 100644 infrastructure/configs/lnd.conf create mode 100644 infrastructure/docker-compose.yml create mode 100755 infrastructure/lnd-init.sh create mode 100755 infrastructure/ops.sh create mode 100755 infrastructure/setup.sh diff --git a/infrastructure/configs/bitcoin.conf b/infrastructure/configs/bitcoin.conf new file mode 100644 index 0000000..78a2dfc --- /dev/null +++ b/infrastructure/configs/bitcoin.conf @@ -0,0 +1,30 @@ +# Bitcoin Core — mainnet +# Placed at /data/bitcoin/bitcoin.conf on the droplet + +# Network +server=1 +listen=1 +bind=0.0.0.0 + +# RPC (internal Docker network only — never exposed publicly) +rpcbind=0.0.0.0 +rpcallowip=172.0.0.0/8 +rpcuser=satoshi +# rpcpassword is set by setup.sh — do not hardcode here + +# ZMQ — LND uses these to get notified of new blocks/txs +zmqpubrawblock=tcp://0.0.0.0:28332 +zmqpubrawrx=tcp://0.0.0.0:28333 + +# Performance +dbcache=512 +maxmempool=300 +maxconnections=40 + +# Prune options — choose one: +# Full node (recommended, ~600GB): comment out both prune lines +# Pruned (~10GB, lower cost but loses historical tx data): +# prune=10240 + +# Logging +debug=0 diff --git a/infrastructure/configs/lnd.conf b/infrastructure/configs/lnd.conf new file mode 100644 index 0000000..ac9ba50 --- /dev/null +++ b/infrastructure/configs/lnd.conf @@ -0,0 +1,32 @@ +[Application Options] +# Human-readable node alias visible on the Lightning Network +alias=timmy-node +color=#F7931A + +# REST API (LNbits connects here — Docker internal only) +restlisten=0.0.0.0:8080 +rpclisten=0.0.0.0:10009 + +# Allow connections from Docker network +tlsextraip=lnd +tlsextradomain=lnd + +# Max pending channels +maxpendingchannels=5 + +[Bitcoin] +bitcoin.active=1 +bitcoin.mainnet=1 +bitcoin.node=bitcoind + +[Bitcoind] +bitcoind.rpchost=bitcoin:8332 +bitcoind.rpcuser=satoshi +# bitcoind.rpcpass is written by setup.sh at runtime +bitcoind.zmqpubrawblock=tcp://bitcoin:28332 +bitcoind.zmqpubrawrx=tcp://bitcoin:28333 + +[tor] +# Optional: enable Tor for node privacy +# tor.active=1 +# tor.v3=1 diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml new file mode 100644 index 0000000..24d0c76 --- /dev/null +++ b/infrastructure/docker-compose.yml @@ -0,0 +1,87 @@ +services: + + bitcoin: + image: lncm/bitcoind:v27.0 + container_name: bitcoin + restart: unless-stopped + volumes: + - bitcoin_data:/data/.bitcoin + ports: + - "8333:8333" + networks: + - node-net + healthcheck: + test: ["CMD", "bitcoin-cli", "-conf=/data/.bitcoin/bitcoin.conf", "getblockchaininfo"] + interval: 60s + timeout: 10s + retries: 5 + start_period: 30s + + lnd: + image: lightningnetwork/lnd:v0.18.3-beta + container_name: lnd + restart: unless-stopped + depends_on: + bitcoin: + condition: service_healthy + volumes: + - lnd_data:/root/.lnd + ports: + - "9735:9735" + networks: + - node-net + healthcheck: + test: ["CMD", "lncli", "--network=mainnet", "state"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 60s + + lnbits: + image: lnbits/lnbits:latest + container_name: lnbits + restart: unless-stopped + depends_on: + lnd: + condition: service_started + ports: + - "127.0.0.1:5000:5000" + volumes: + - lnbits_data:/app/data + - lnd_data:/lnd:ro + environment: + - LNBITS_DATA_FOLDER=/app/data + - LNBITS_BACKEND_WALLET_CLASS=LndRestWallet + - LND_REST_ENDPOINT=https://lnd:8080 + - LND_REST_CERT=/lnd/tls.cert + - LND_REST_MACAROON=/lnd/data/chain/bitcoin/mainnet/invoice.macaroon + - LNBITS_SITE_TITLE=Timmy Node + - LNBITS_SITE_TAGLINE=Lightning AI Agent Infrastructure + - UVICORN_HOST=0.0.0.0 + - UVICORN_PORT=5000 + networks: + - node-net + +networks: + node-net: + driver: bridge + +volumes: + bitcoin_data: + driver: local + driver_opts: + type: none + o: bind + device: /data/bitcoin + lnd_data: + driver: local + driver_opts: + type: none + o: bind + device: /data/lnd + lnbits_data: + driver: local + driver_opts: + type: none + o: bind + device: /data/lnbits diff --git a/infrastructure/lnd-init.sh b/infrastructure/lnd-init.sh new file mode 100755 index 0000000..e0e6ffe --- /dev/null +++ b/infrastructure/lnd-init.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# ============================================================ +# LND wallet initialization + LNbits startup +# Run AFTER Bitcoin sync is complete (verificationprogress ~1.0) +# Run as root on the droplet: bash /opt/timmy-node/lnd-init.sh +# ============================================================ +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[lnd-init]${NC} $*"; } +ok() { echo -e "${GREEN}[ok]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +die() { echo -e "${RED}[error]${NC} $*"; exit 1; } + +INFRA_DIR="/opt/timmy-node" +CREDS_FILE="/root/node-credentials.txt" + +# ── Check Bitcoin sync ─────────────────────────────────────── +info "Checking Bitcoin sync status..." +PROGRESS=$(docker exec bitcoin bitcoin-cli getblockchaininfo 2>/dev/null | jq -r .verificationprogress || echo "0") +info "Chain sync progress: $PROGRESS" +if (( $(echo "$PROGRESS < 0.999" | bc -l) )); then + warn "Bitcoin is not fully synced yet (progress: $PROGRESS)" + warn "LND needs a synced chain to function correctly." + read -rp "Continue anyway? (y/N) " CONFIRM + [[ "$CONFIRM" != "y" ]] && die "Aborting. Run again when sync is complete." +fi + +# ── Start LND ──────────────────────────────────────────────── +info "Starting LND..." +cd "$INFRA_DIR" +docker compose up -d lnd +sleep 5 + +# ── Wallet init ────────────────────────────────────────────── +echo "" +echo -e "${CYAN}══════════════════════════════════════════════${NC}" +echo -e "${CYAN} LND Wallet Setup${NC}" +echo -e "${CYAN}══════════════════════════════════════════════${NC}" +echo "" +echo -e " Choose one:" +echo -e " ${GREEN}c${NC} — Create a NEW wallet (generates a fresh seed phrase)" +echo -e " ${YELLOW}r${NC} — Restore from existing 24-word seed" +echo "" +read -rp " Your choice [c/r]: " WALLET_CHOICE + +if [[ "$WALLET_CHOICE" == "r" ]]; then + info "Restoring wallet from seed..." + docker exec -it lnd lncli --network=mainnet create --recovery-window=2500 +else + info "Creating new wallet..." + docker exec -it lnd lncli --network=mainnet create +fi + +echo "" +echo -e "${RED}══════════════════════════════════════════════${NC}" +echo -e "${RED} CRITICAL: Write down your 24-word seed phrase${NC}" +echo -e "${RED} It was shown above. Store it offline, safely.${NC}" +echo -e "${RED} This is the ONLY way to recover your funds.${NC}" +echo -e "${RED}══════════════════════════════════════════════${NC}" +echo "" +read -rp " I have written down my seed phrase. Press enter to continue..." + +# ── Wait for LND to be ready ───────────────────────────────── +info "Waiting for LND to be ready..." +for i in {1..30}; do + STATE=$(docker exec lnd lncli --network=mainnet state 2>/dev/null | jq -r .state || echo "") + if [[ "$STATE" == "SERVER_ACTIVE" ]]; then + ok "LND is active" + break + fi + echo -n "." + sleep 5 +done +echo "" + +# ── Get LND pubkey ─────────────────────────────────────────── +PUBKEY=$(docker exec lnd lncli --network=mainnet getinfo 2>/dev/null | jq -r .identity_pubkey || echo "") +if [[ -n "$PUBKEY" ]]; then + ok "LND node pubkey: $PUBKEY" + echo "LND_PUBKEY=$PUBKEY" >> "$CREDS_FILE" +fi + +# ── Start LNbits ───────────────────────────────────────────── +info "Starting LNbits..." +docker compose up -d lnbits +sleep 8 + +# ── Wait for LNbits ────────────────────────────────────────── +for i in {1..20}; do + if curl -sf http://127.0.0.1:5000/api/v1/health &>/dev/null; then + ok "LNbits is up" + break + fi + echo -n "." + sleep 3 +done +echo "" + +# ── Enable Tailscale Funnel for LNbits ─────────────────────── +info "Exposing LNbits via Tailscale Funnel (public HTTPS)..." +tailscale serve --bg http://127.0.0.1:5000 +tailscale funnel --bg 443 + +TAILSCALE_HOSTNAME=$(tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//') +LNBITS_URL="https://$TAILSCALE_HOSTNAME" + +ok "LNbits is available at: $LNBITS_URL" +echo "LNBITS_URL=$LNBITS_URL" >> "$CREDS_FILE" + +# ── Create Timmy wallet in LNbits ──────────────────────────── +info "Creating Timmy wallet in LNbits..." +sleep 3 + +# Create a user + wallet via LNbits API +LNBITS_ADMIN_KEY=$(curl -sf http://127.0.0.1:5000/api/v1/wallets \ + -H "Content-Type: application/json" \ + | jq -r '.[0].adminkey' 2>/dev/null || echo "") + +if [[ -z "$LNBITS_ADMIN_KEY" ]]; then + warn "Could not auto-create wallet. Open $LNBITS_URL in browser," + warn "create a wallet called 'Timmy', and copy the Invoice Key." +else + # Create a dedicated Timmy wallet + TIMMY_WALLET=$(curl -sf http://127.0.0.1:5000/api/v1/account \ + -X POST \ + -H "Content-Type: application/json" \ + -H "X-Api-Key: $LNBITS_ADMIN_KEY" \ + -d '{"name":"Timmy"}' 2>/dev/null || echo "{}") + + TIMMY_INVOICE_KEY=$(echo "$TIMMY_WALLET" | jq -r '.wallets[0].inkey' 2>/dev/null || echo "") + + if [[ -n "$TIMMY_INVOICE_KEY" ]]; then + ok "Timmy wallet created" + echo "LNBITS_API_KEY=$TIMMY_INVOICE_KEY" >> "$CREDS_FILE" + fi +fi + +# ── Final output ───────────────────────────────────────────── +echo "" +echo -e "${GREEN}════════════════════════════════════════════════${NC}" +echo -e "${GREEN} LND + LNbits ready — set these in Replit:${NC}" +echo -e "${GREEN}════════════════════════════════════════════════${NC}" +echo "" +grep "LNBITS_URL\|LNBITS_API_KEY" "$CREDS_FILE" 2>/dev/null || true +echo "" +echo -e " If LNBITS_API_KEY is blank above, open $LNBITS_URL," +echo -e " create a wallet called 'Timmy', and copy its Invoice Key." +echo "" +echo -e " ${CYAN}Next: fund your Lightning node${NC}" +echo -e " Get your on-chain address:" +echo -e " docker exec lnd lncli --network=mainnet newaddress p2wkh" +echo -e " Send at least 0.001 BTC to open your first channel." +echo "" +echo -e " Full credentials: ${YELLOW}cat $CREDS_FILE${NC}" +echo "" diff --git a/infrastructure/ops.sh b/infrastructure/ops.sh new file mode 100755 index 0000000..3e179a4 --- /dev/null +++ b/infrastructure/ops.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# ============================================================ +# Timmy Node — Day-to-day operations helper +# Usage: bash ops.sh +# ============================================================ + +INFRA_DIR="/opt/timmy-node" +cd "$INFRA_DIR" 2>/dev/null || { echo "Run on the droplet, not locally"; exit 1; } + +CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' + +case "${1:-help}" in + + status) + echo -e "\n${CYAN}── Docker services ──────────────────────────${NC}" + docker compose ps + + echo -e "\n${CYAN}── Bitcoin sync ──────────────────────────────${NC}" + docker exec bitcoin bitcoin-cli getblockchaininfo 2>/dev/null \ + | jq '{blocks, headers, verificationprogress, size_on_disk}' \ + || echo "Bitcoin not ready" + + echo -e "\n${CYAN}── LND state ─────────────────────────────────${NC}" + docker exec lnd lncli --network=mainnet getinfo 2>/dev/null \ + | jq '{identity_pubkey, alias, num_peers, num_active_channels, synced_to_chain}' \ + || echo "LND not ready" + + echo -e "\n${CYAN}── LND wallet balance ────────────────────────${NC}" + docker exec lnd lncli --network=mainnet walletbalance 2>/dev/null \ + | jq '{confirmed_balance, unconfirmed_balance}' \ + || true + + echo -e "\n${CYAN}── LND channel balance ───────────────────────${NC}" + docker exec lnd lncli --network=mainnet channelbalance 2>/dev/null \ + | jq '{balance, pending_open_balance}' \ + || true + ;; + + sync) + watch -n 10 'docker exec bitcoin bitcoin-cli getblockchaininfo \ + | jq "{blocks, verificationprogress, size_on_disk}"' + ;; + + logs) + SERVICE="${2:-}" + if [[ -z "$SERVICE" ]]; then + docker compose logs -f --tail=50 + else + docker compose logs -f --tail=100 "$SERVICE" + fi + ;; + + restart) + SERVICE="${2:-}" + if [[ -z "$SERVICE" ]]; then + docker compose restart + else + docker compose restart "$SERVICE" + fi + ;; + + fund) + echo -e "${CYAN}Your on-chain deposit address (send BTC here to fund channels):${NC}" + docker exec lnd lncli --network=mainnet newaddress p2wkh | jq -r .address + ;; + + channels) + echo -e "\n${CYAN}── Active channels ───────────────────────────${NC}" + docker exec lnd lncli --network=mainnet listchannels 2>/dev/null \ + | jq '.channels[] | {remote_pubkey, capacity, local_balance, remote_balance, active}' + echo -e "\n${CYAN}── Pending channels ──────────────────────────${NC}" + docker exec lnd lncli --network=mainnet pendingchannels 2>/dev/null \ + | jq '{pending_open_channels: .pending_open_channels | length, pending_closing_channels: .pending_closing_channels | length}' + ;; + + open-channel) + echo -e "${CYAN}Open a channel — usage:${NC}" + echo " bash ops.sh open-channel @: " + if [[ -n "${2:-}" && -n "${3:-}" ]]; then + docker exec lnd lncli --network=mainnet connect "$2" 2>/dev/null || true + PUBKEY=$(echo "$2" | cut -d@ -f1) + docker exec lnd lncli --network=mainnet openchannel \ + --node_key "$PUBKEY" \ + --local_amt "$3" \ + --push_amt 0 + fi + ;; + + lnbits-key) + echo -e "${CYAN}LNbits API keys for Timmy wallet:${NC}" + grep "LNBITS" /root/node-credentials.txt 2>/dev/null || \ + echo "Check /root/node-credentials.txt — or open LNbits dashboard" + TSHOST=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName' | sed 's/\.$//') + echo -e "\nLNbits dashboard: ${GREEN}https://$TSHOST${NC}" + ;; + + update) + echo -e "${YELLOW}Pulling latest Docker images...${NC}" + docker compose pull + docker compose up -d + ok "Done" + ;; + + backup) + BACKUP_FILE="/root/lnd-backup-$(date +%Y%m%d-%H%M%S).tar.gz" + echo -e "${CYAN}Backing up LND channel state to $BACKUP_FILE${NC}" + tar -czf "$BACKUP_FILE" -C /data/lnd . + echo -e "${GREEN}Backup saved. Copy this file off the server:${NC}" + echo " scp root@:$BACKUP_FILE ./lnd-backup.tar.gz" + ;; + + help|*) + echo -e "\n${CYAN}Timmy Node operations:${NC}" + echo "" + echo " bash ops.sh status — overview of all services + balances" + echo " bash ops.sh sync — watch Bitcoin chain sync progress" + echo " bash ops.sh logs [service] — tail logs (bitcoin | lnd | lnbits)" + echo " bash ops.sh restart [svc] — restart a service or all services" + echo " bash ops.sh fund — get on-chain deposit address" + echo " bash ops.sh channels — list open and pending channels" + echo " bash ops.sh open-channel — open a Lightning channel" + echo " bash ops.sh lnbits-key — show LNBITS_URL and API key for Replit" + echo " bash ops.sh update — pull latest Docker images" + echo " bash ops.sh backup — backup LND channel state" + echo "" + ;; + +esac diff --git a/infrastructure/setup.sh b/infrastructure/setup.sh new file mode 100755 index 0000000..e0758d3 --- /dev/null +++ b/infrastructure/setup.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# ============================================================ +# Timmy Bitcoin Node — One-shot bootstrap for Ubuntu 22.04 LTS +# Run as root on a fresh Digital Ocean droplet: +# bash setup.sh +# ============================================================ +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[setup]${NC} $*"; } +ok() { echo -e "${GREEN}[ok]${NC} $*"; } +warn() { echo -e "${YELLOW}[warn]${NC} $*"; } +die() { echo -e "${RED}[error]${NC} $*"; exit 1; } + +[[ $EUID -ne 0 ]] && die "Run as root: sudo bash setup.sh" + +# ── 0. Config ──────────────────────────────────────────────── +DATA_DIR="/data" +INFRA_DIR="/opt/timmy-node" +RPC_PASS=$(openssl rand -hex 24) + +info "Starting Timmy node setup..." +echo "" + +# ── 1. System packages ─────────────────────────────────────── +info "Updating system packages..." +apt-get update -qq +apt-get upgrade -y -qq +apt-get install -y -qq curl wget git ufw jq openssl + +# ── 2. Docker ──────────────────────────────────────────────── +info "Installing Docker..." +if ! command -v docker &>/dev/null; then + curl -fsSL https://get.docker.com | sh + systemctl enable docker + systemctl start docker +fi +docker --version +ok "Docker ready" + +# ── 3. Tailscale ───────────────────────────────────────────── +info "Installing Tailscale..." +if ! command -v tailscale &>/dev/null; then + curl -fsSL https://tailscale.com/install.sh | sh +fi +ok "Tailscale installed — you will authenticate it after this script finishes" + +# ── 4. Firewall ────────────────────────────────────────────── +info "Configuring UFW firewall..." +ufw --force reset +# Allow Tailscale +ufw allow in on tailscale0 +# Bitcoin P2P +ufw allow 8333/tcp comment "Bitcoin P2P" +# Lightning P2P +ufw allow 9735/tcp comment "Lightning P2P" +# SSH via Tailscale only — but keep public SSH open until Tailscale is confirmed +# (after Tailscale auth, you can run: ufw delete allow 22) +ufw allow 22/tcp comment "SSH (disable after Tailscale is confirmed)" +ufw default deny incoming +ufw default allow outgoing +ufw --force enable +ok "Firewall configured" + +# ── 5. Mount the DO block volume ───────────────────────────── +info "Setting up data volume..." +# Digital Ocean attaches block volumes at /dev/sda or /dev/disk/by-id/scsi-... +# Find the attached volume (should be the largest unmounted disk) +VOLUME_DEV=$(lsblk -rno NAME,SIZE,MOUNTPOINT | awk '$3=="" && $2~/G/ {print $1}' | grep -v "^sda$" | head -1) + +if [[ -z "$VOLUME_DEV" ]]; then + warn "Could not auto-detect a block volume. If you attached a DO Volume," + warn "run: lsblk to find it, then: mkfs.ext4 /dev/ && mount /dev/ /data" + warn "Then re-run this script or continue manually." + mkdir -p "$DATA_DIR" +else + VOLUME_PATH="/dev/$VOLUME_DEV" + info "Found volume at $VOLUME_PATH" + # Format only if not already formatted + if ! blkid "$VOLUME_PATH" &>/dev/null; then + info "Formatting $VOLUME_PATH with ext4..." + mkfs.ext4 -F "$VOLUME_PATH" + fi + mkdir -p "$DATA_DIR" + mount "$VOLUME_PATH" "$DATA_DIR" + # Persist mount across reboots + BLKID=$(blkid -s UUID -o value "$VOLUME_PATH") + if ! grep -q "$BLKID" /etc/fstab; then + echo "UUID=$BLKID $DATA_DIR ext4 defaults,nofail 0 2" >> /etc/fstab + fi + ok "Volume mounted at $DATA_DIR" +fi + +# ── 6. Directory structure ─────────────────────────────────── +info "Creating directory structure..." +mkdir -p \ + "$DATA_DIR/bitcoin" \ + "$DATA_DIR/lnd" \ + "$DATA_DIR/lnbits" \ + "$INFRA_DIR/configs" +ok "Directories ready" + +# ── 7. Copy configs & inject RPC password ──────────────────── +info "Installing configs..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Bitcoin config +cp "$SCRIPT_DIR/configs/bitcoin.conf" "$DATA_DIR/bitcoin/bitcoin.conf" +echo "rpcpassword=$RPC_PASS" >> "$DATA_DIR/bitcoin/bitcoin.conf" + +# LND config — inject rpcpass +cp "$SCRIPT_DIR/configs/lnd.conf" "$INFRA_DIR/configs/lnd.conf" +sed -i "s/# bitcoind.rpcpass is written by setup.sh at runtime/bitcoind.rpcpass=$RPC_PASS/" \ + "$INFRA_DIR/configs/lnd.conf" + +# Mount lnd.conf into container via docker-compose override +cat > "$INFRA_DIR/docker-compose.override.yml" < "$CREDS_FILE" < (via Tailscale MagicDNS)" +echo -e " Then lock down public SSH:" +echo -e " ufw delete allow 22" +echo "" +echo -e " 3. ${CYAN}Check Bitcoin sync progress:${NC}" +echo -e " watch -n 10 'docker exec bitcoin bitcoin-cli getblockchaininfo | jq .verificationprogress'" +echo -e " (will show 0.0 → 1.0 over several days)" +echo "" +echo -e " 4. ${CYAN}Once sync reaches ~1.0, initialize LND:${NC}" +echo -e " bash $INFRA_DIR/lnd-init.sh" +echo "" +echo -e " Credentials: ${YELLOW}cat $CREDS_FILE${NC}" +echo ""