Merge pull request #1193 from starius/loop-out-validation-reserves

loopd: account for channel reserves in loop out validation
This commit is contained in:
Boris Nagaev 2026-08-11 10:07:26 -05:00 committed by GitHub
commit aa79fa5262
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 6 deletions

View file

@ -6,6 +6,10 @@
#### Bug Fixes #### Bug Fixes
* Loop Out requests now account for channel reserves when checking outbound
capacity, preventing swaps from starting when their off-chain payment cannot
be funded.
* Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, * Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates,
keeps cached asset-name lookups responsive during slow `tapd` queries, and keeps cached asset-name lookups responsive during slow `tapd` queries, and
closes `tapd` connections cleanly during shutdown and startup failures. closes `tapd` connections cleanly during shutdown and startup failures.

View file

@ -3013,6 +3013,13 @@ func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient,
// the amount requested, the maximum possible routing fees, // the amount requested, the maximum possible routing fees,
// the available channel set and the fact that equal splitting is // the available channel set and the fact that equal splitting is
// used for MPP. // used for MPP.
//
// TODO: Also account for the quoted server fee and the concurrent prepay
// payment. The CLI and autoloop obtain the fee and prepay amounts from a
// quote, but this RPC only carries maximum limits, which direct callers may
// set higher than the quoted amounts. Treating those limits as exact can
// reject a viable swap, while the actual routing fees are only known when
// the invoices are paid.
requiredBalance := btcutil.Amount(req.Amt + req.MaxSwapRoutingFee) requiredBalance := btcutil.Amount(req.Amt + req.MaxSwapRoutingFee)
isRoutable, _ := hasBandwidth(activeChannelSet, requiredBalance, isRoutable, _ := hasBandwidth(activeChannelSet, requiredBalance,
int(maxParts)) int(maxParts))
@ -3047,11 +3054,24 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount,
localBalances := make([]btcutil.Amount, len(channels)) localBalances := make([]btcutil.Amount, len(channels))
var totalBandwidth btcutil.Amount var totalBandwidth btcutil.Amount
for i, channel := range channels { for i, channel := range channels {
tracef("Channel %v: local=%v remote=%v", channel.ChannelID, localBalance := channel.LocalBalance
channel.LocalBalance, channel.RemoteBalance) var reserve btcutil.Amount
if channel.LocalConstraints != nil {
reserve = channel.LocalConstraints.Reserve
}
localBalances[i] = channel.LocalBalance if reserve >= localBalance {
totalBandwidth += channel.LocalBalance localBalance = 0
} else {
localBalance -= reserve
}
tracef("Channel %v: local=%v reserve=%v available=%v "+
"remote=%v", channel.ChannelID, channel.LocalBalance,
reserve, localBalance, channel.RemoteBalance)
localBalances[i] = localBalance
totalBandwidth += localBalance
} }
tracef("Total bandwidth: %v", totalBandwidth) tracef("Total bandwidth: %v", totalBandwidth)
@ -3073,8 +3093,6 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount,
paid := false paid := false
for i := range len(localBalances) { for i := range len(localBalances) {
// TODO(hieblmi): Consider channel reserves because the
// channel can't send its full local balance.
if localBalances[i] >= split { if localBalances[i] >= split {
tracef("len(shards)=%v: Local channel "+ tracef("len(shards)=%v: Local channel "+
"balance %v can pay %v sats", "balance %v can pay %v sats",

View file

@ -1378,6 +1378,50 @@ func TestValidateLoopOutRequest(t *testing.T) {
err: errBalanceTooLow, err: errBalanceTooLow,
expectedTarget: 0, expectedTarget: 0,
}, },
{
name: "channel reserve leaves balance one sat short",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
{
Active: true,
ChannelID: chanID2.ToUint64(),
LocalBalance: 10100,
LocalConstraints: &lndclient.ChannelConstraints{
Reserve: 100,
},
},
},
amount: 10000,
maxRoutingFee: 1,
maxParts: 1,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "channel reserve leaves exact balance",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
{
Active: true,
ChannelID: chanID2.ToUint64(),
LocalBalance: 10101,
LocalConstraints: &lndclient.ChannelConstraints{
Reserve: 100,
},
},
},
amount: 10000,
maxRoutingFee: 1,
maxParts: 1,
err: nil,
expectedTarget: 2,
},
{ {
name: "can split between channels", name: "can split between channels",
chain: chaincfg.MainNetParams, chain: chaincfg.MainNetParams,