diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 08:45:27 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-29 16:40:36 -0700 |
| commit | abb0f094aa51155cec4561b4999f6739df47f23e (patch) | |
| tree | 14547b0c82689a99f4033b0fc3577471f9ab9d48 /lib | |
| parent | c7c0bcdb552a1e8f080abdf299e544d82ce9882a (diff) | |
| download | coinos-server-abb0f094aa51155cec4561b4999f6739df47f23e.tar.gz coinos-server-abb0f094aa51155cec4561b4999f6739df47f23e.zip | |
lnquote: keep the quote layer until a timed-out getroutes is done
askrene queues getroutes calls beyond its child limit and only checks layer
names when they arrive. Removing the quote layer after our RPC timeout left
a queued request pointing at a missing layer, which trips an assert that
takes lightningd down. Delay the removal when getroutes never answered.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/lnquote.ts | 29 |
1 files changed, 24 insertions, 5 deletions
diff --git a/lib/lnquote.ts b/lib/lnquote.ts index de96f69a..bc34b3fb 100644 --- a/lib/lnquote.ts +++ b/lib/lnquote.ts @@ -22,6 +22,9 @@ const FAKE_NODE = const XPAY_MAXDELAY = 2016; // xpay's default maxdelay +// How long a quote layer outlives a getroutes call that timed out on our side +const LAYER_REMOVE_DELAY = 10 * 60_000; + // Extra layers sendLightning passes to xpay. xpay forwards user layers to // getroutes verbatim, and getroutes rejects an unknown layer outright, so only // pass the ones that actually exist on this node (prod has prefer-kappa, @@ -173,6 +176,12 @@ export const quoteRoutingFee = async ({ } const layer = `quote-${v4()}`; + // True while a getroutes on this layer may still be queued inside askrene. + // askrene runs at most 4 route computations at once and checks layer names + // only on arrival; a queued request whose layer was removed in the meantime + // trips an assert in apply_layers that aborts lightningd. When our RPC times + // out the request is still in that queue, so the layer has to outlive it. + let routing = false; try { await ln.call("askrene-create-layer", { layer, persistent: false }); for (const f of fakes) { @@ -195,6 +204,7 @@ export const quoteRoutingFee = async ({ }); } + routing = true; const r = await ln.call("getroutes", { source: me, destination, @@ -211,7 +221,12 @@ export const quoteRoutingFee = async ({ maxfee_msat: cap, final_cltv: finalCltv, maxdelay: XPAY_MAXDELAY, + }).catch((e) => { + // Any reply from askrene, error or not, means it is done with the layer + if (e?.code !== "ETIMEDOUT") routing = false; + throw e; }); + routing = false; const routes: any[] = r?.routes ?? []; if (!routes.length) fail("No route found"); @@ -232,11 +247,15 @@ export const quoteRoutingFee = async ({ const feeMsat = Math.max(0, sent - delivered); return { fee: Math.ceil(feeMsat / 1000), feeMsat, parts: routes.length }; } finally { - try { - await ln.call("askrene-remove-layer", { layer }); - } catch (e: any) { - warn("askrene-remove-layer failed", layer, e?.message ?? String(e)); - } + const remove = async () => { + try { + await ln.call("askrene-remove-layer", { layer }); + } catch (e: any) { + warn("askrene-remove-layer failed", layer, e?.message ?? String(e)); + } + }; + if (routing) setTimeout(remove, LAYER_REMOVE_DELAY); + else await remove(); } }; |
