fix(lnd): pay through Router.SendPaymentV2 — LND 0.21 removed the old route

LND 0.21.2 removed the deprecated Lightning.SendPaymentSync REST route
(/v1/channels/transactions). The backend still called it, so every
Lightning send answered literal HTTP 404 and the wallet UI reported
'Payment failed: Not Found' fleet-wide right after the pin bump —
receive worked, which made it look intermittent.

Pay through the supported Router.SendPaymentV2 route (/v2/router/send)
instead, keeping the existing contract with the UI:
- single-record responses (no_inflight_updates) unwrapped from the
  grpc-gateway result envelope, transport errors from the nested error
- a slow multi-hop payment still resolves as pending + payment hash
  (only LND may declare failure), never a false 'Payment failed'
- LND's failure_reason codes translated to the same plain-language
  advice, invoice-expiry still says 'ask for a fresh invoice'

Guard it at the gate: tests/lifecycle/bats/lnd-api-compat.bats POSTs a
deliberately-invalid invoice to /v2/router/send on the RUNNING LND and
fails if the route answers 404 — the image/backend skew that shipped
silently last time because no test ever spoke the payment endpoint.
Also bumps the stale lnd image expectation in remote-lifecycle.sh.
This commit is contained in:
archipelago
2026-09-01 10:28:49 -04:00
parent 9fb2e1ed9e
commit cbd5314dd9
3 changed files with 196 additions and 50 deletions
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bats
# tests/lifecycle/bats/lnd-api-compat.bats
#
# Regression guard for the 2026-09-01 fleet breakage: LND 0.21 REMOVED the
# deprecated Lightning.SendPaymentSync REST route (`/v1/channels/transactions`)
# that the backend paid through — every Lightning send answered the literal
# HTTP 404 "Not Found" and the wallet UI showed "Payment failed: Not Found".
# The backend now pays via Router.SendPaymentV2 (`/v2/router/send`).
#
# This test does not send sats. It POSTs a deliberately-invalid invoice to the
# v2 route on the RUNNING LND and asserts the route itself answers: a
# 400/500 "cannot parse" proves the endpoint exists; a 404 means the pinned
# image no longer serves the route the backend calls — the exact image/backend
# skew that shipped silently last time because no gate test ever spoke the
# payment endpoint.
#
# Tiers: read-only (invalid payment request; nothing is sent).
#
# Runs on the archy host (sudo for the macaroon, curl to localhost).
LND_MAINNET_DIR="/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet"
_lnd_rest_host_port() {
local mf
for mf in \
"${ARCHIPELAGO_APPS_DIR:-/opt/archipelago/apps}/lnd/manifest.yml" \
"${ARCHIPELAGO_APPS_DIR:-/opt/archipelago/apps}/lnd/manifest.yaml" \
"$BATS_TEST_DIRNAME/../../../apps/lnd/manifest.yml"; do
[[ -r "$mf" ]] || continue
awk '
/- host:/ { host=$3 }
/container:/ { if ($2 == 8080 && host != "") { print host; exit } }
' "$mf"
return 0
done
}
@test "running LND serves /v2/router/send (the route the backend pays through)" {
if ! podman ps --format '{{.Names}}' 2>/dev/null | grep -qx lnd; then
skip "lnd not running"
fi
local port
port=$(_lnd_rest_host_port)
[[ -n "$port" ]] || skip "could not resolve LND REST host port from manifest"
local mac
mac=$(sudo cat "$LND_MAINNET_DIR/admin.macaroon" 2>/dev/null | od -An -tx1 -v | tr -d " \n")
[[ -n "$mac" ]] || skip "LND admin macaroon not readable (LND installed but wallet not initialized?)"
local code body
body=$(mktemp)
code=$(curl -sk -o "$body" -w '%{http_code}' --max-time 10 -X POST \
-H "Grpc-Metadata-macaroon: $mac" \
-H 'Content-Type: application/json' \
--data '{"payment_request":"lnbc1notarealinvoice","timeout_seconds":5,"no_inflight_updates":true}' \
"https://127.0.0.1:${port}/v2/router/send" || echo 000)
rm -f "$body"
# 000 = LND REST unreachable at all — that is port-drift's failure class
# (port-drift.bats), but it also breaks payments, so fail loudly here too.
if [[ "$code" == "000" ]]; then
fail "LND REST not reachable on ${port} — payments cannot be sent at all"
fi
if [[ "$code" == "404" ]]; then
fail "running LND does not serve /v2/router/send (HTTP 404) — the backend's payment route is gone; every Lightning send fails 'Not Found'"
fi
}
@test "backend no longer references the removed /v1/channels/transactions route" {
# Source-level guard: the removed route must not creep back into the
# payment path (the runtime fix is in api/rpc/lnd/payments.rs).
local src="$BATS_TEST_DIRNAME/../../../core/archipelago/src/api/rpc/lnd/payments.rs"
[[ -r "$src" ]] || skip "source tree not present"
if grep -q 'v1/channels/transactions' "$src"; then
fail "payments.rs references /v1/channels/transactions — removed in LND 0.21, answers 404"
fi
}
+1 -1
View File
@@ -136,7 +136,7 @@ image_for() {
bitcoin-knots) echo "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest" ;;
bitcoin-core) echo "docker.io/bitcoin/bitcoin:28.4" ;;
btcpay-server) echo "docker.io/btcpayserver/btcpayserver:2.4.2" ;;
lnd) echo "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta" ;;
lnd) echo "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta" ;;
mempool) echo "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.0" ;;
homeassistant) echo "source.archipelago-foundation.org/lfg2025/home-assistant:2024.1" ;;
grafana) echo "source.archipelago-foundation.org/lfg2025/grafana:10.2.0" ;;