fix(lnd): SendPaymentV2 needs an explicit fee budget — absent means ZERO
Demo images / Build & push demo images (push) Successful in 3m28s

v1.8.9's move to Router.SendPaymentV2 shipped without fee_limit_sat,
and the v2 route treats an ABSENT fee limit as zero allowed fees.
Every real route carries a routing fee (the 2-hop route here: 1.5
sats), so the pathfinder rejected them all and the wallet answered
"No route to the recipient" on EVERY send — all day, on healthy
channels with plenty of liquidity both ways.

The router debug log makes it unambiguous:
  wallet payment (v1.8.9 backend): fee_limit=0 mSAT     -> no route
  same payment by hand (lncli --fee_limit=100): fee_limit=100000 mSAT -> settles in 0.65s

My earlier "pipeline verified" claim was wrong — the manual lncli
verification set a fee limit by hand and masked this exact bug. The
400k that succeeded this morning went through the pre-update backend
on the pre-update LND.

Payments now carry lncli's own default budget — the payment amount
(100%), preferring the payer-supplied amount for zero-value invoices
and the invoice's own amount otherwise, with a nominal floor so the
limit can never be zero. Unit-pinned so it cannot regress.
This commit is contained in:
archipelago
2026-09-01 18:42:37 -04:00
parent 0d0e2e243a
commit 1ca002661b
3 changed files with 37 additions and 0 deletions
+2
View File
@@ -2,6 +2,8 @@
## v1.8.10-alpha (2026-09-02)
- **Lightning sends work again — v1.8.9's payment switch lost the fee budget.** Moving payments to LND 0.21's supported route (Router.SendPaymentV2) shipped without a fee limit, and the v2 API treats an absent limit as **zero allowed fees**: every real route carries a routing fee, so the pathfinder rejected them all and the wallet answered "No route to the recipient" on every send — all day, on healthy channels with plenty of liquidity. The router debug log made it unambiguous (`fee_limit=0 mSAT` on every failing wallet payment; the same payment succeeded by hand the moment a fee limit was set). Payments now carry lncli's default budget (the payment amount), the wallet's amount handling for zero-value invoices is preserved, and a unit test pins the limit can never be zero again.
- **A channel that drops its peer link now heals itself — on every node.** Restarting LND (an app update, a reboot, container churn) can leave a channel's peer connection down for hours while both endpoints keep the channel flagged disabled in the routing graph: the node looks perfectly healthy, the wallet shows balance, and every payment in either direction fails "no route to the recipient". Observed live: a node's only channel sat unroutable for ~17 hours after the LND 0.21.2 update, with no sign of it in any dashboard. The daemon now watches the channel graph as desired state — every open channel should have a live peer — and reconnects any that don't, using the peer's advertised addresses. Nodes without LND are untouched; an unreachable peer is retried gently, not hammered.
- **The Lightning wallet states the node's real funding state instead of "you have no channel."** Trying to send while a freshly opened channel was still waiting for on-chain confirmations — or when all its balance sits on the far side — raised a modal that claimed the node had NO channel at all (the outbound sum is legitimately zero in both states), pointed the user at opening a second channel, and — for payment routing failures — even showed the *receiving* copy. The funding gate now reads the channel list it already fetched: a confirming channel gets "it unlocks automatically once confirmed, nothing is needed from you", a far-side balance gets "you can receive, but there's nothing to send right now", a routing/liquidity payment failure says so instead of claiming channel problems, and only a genuinely channel-less node keeps the open-one guidance.
@@ -42,6 +42,21 @@ fn json_i64(value: &serde_json::Value, key: &str) -> Option<i64> {
})
}
/// Fee budget for a send, matching lncli's own default: the payment amount
/// (100%). Zero-amount invoices take the payer-supplied amount; fixed invoices
/// take the invoice's own amount. Falls back to a nominal 1,000 sats only when
/// both are somehow absent — the limit must never be left at LND's zero
/// default, which rejects every fee-carrying route as "no route".
fn fee_limit_sats(amount_sats: Option<u64>, decoded_amt: i64) -> i64 {
if let Some(amt) = amount_sats {
return amt as i64;
}
if decoded_amt > 0 {
return decoded_amt;
}
1_000
}
impl RpcHandler {
/// Pay a Lightning invoice.
pub(in crate::api::rpc) async fn handle_lnd_payinvoice(
@@ -107,6 +122,14 @@ impl RpcHandler {
// enough, and it makes grpc-gateway's response a single JSON value.
"no_inflight_updates": true,
"timeout_seconds": 120,
// Router.SendPaymentV2 treats an ABSENT fee limit as ZERO — every
// real route carries a routing fee, so the pathfinder rejects
// them all and the wallet gets "No route to the recipient" on
// every send (fleet-wide, 2026-09-01: the v1.8.9 switch to the v2
// route shipped without this, and a manual lncli test that set
// --fee_limit masked it). lncli's own default is the payment
// amount (100%), which is what we send here.
"fee_limit_sat": fee_limit_sats(amount_sats, decoded_amt),
});
if let Some(amt) = amount_sats {
pay_body["amt"] = serde_json::json!(amt.to_string());
@@ -550,4 +573,15 @@ mod tests {
"Insufficient channel balance"
);
}
#[test]
fn fee_limit_never_falls_back_to_zero() {
// SendPaymentV2 defaults an ABSENT fee limit to zero — which rejects
// every fee-carrying route as "no route". The budget must always be
// positive: the payer-supplied amount for zero-amount invoices, the
// invoice's own amount otherwise.
assert_eq!(fee_limit_sats(Some(20_000), 0), 20_000);
assert_eq!(fee_limit_sats(None, 20_000), 20_000);
assert_eq!(fee_limit_sats(None, 0), 1_000);
}
}
@@ -369,6 +369,7 @@ init()
<span class="text-xs text-white/40">September 2, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p><strong>Lightning sends work again.</strong> v1.8.9's move to LND 0.21's supported payment route shipped without a fee budget, and the API treats a missing one as zero allowed fees — so every wallet send failed "No route to the recipient" all day, on perfectly healthy channels. Payments now carry a proper fee budget and a test keeps it from ever regressing.</p>
<p><strong>A channel that drops its peer link now heals itself — on every node.</strong> Restarting LND (an app update, a reboot, container churn) can leave a channel's peer connection down for hours while both endpoints keep the channel flagged disabled in the routing graph: the node looks perfectly healthy, the wallet shows balance, and every payment in either direction fails "no route to the recipient". The daemon now watches the channel graph as desired state — every open channel should have a live peer — and reconnects any that don't. Nodes without LND are untouched; an unreachable peer is retried gently.</p>
<p><strong>The Lightning wallet says what's actually wrong, instead of "you have no channel".</strong> Trying to send while a channel you just opened was still confirming — or when all its balance sits on the far side — produced a modal claiming you had no channel at all, and payment routing failures even showed the receiving copy. The gate now reads your real channel list: a confirming channel gets "it unlocks automatically once confirmed, nothing is needed from you", a far-side balance gets "you can receive, but there's nothing to send right now", and only a genuinely channel-less node is sent to open one.</p>
</div>