4444 // errConfTargetTooLow is returned when the chosen confirmation target
4545 // is below the allowed minimum.
4646 errConfTargetTooLow = errors .New ("confirmation target too low" )
47+
48+ // errBalanceTooLow is returned when the loop out amount can't be
49+ // satisfied given total balance of the selection of channels to loop
50+ // out on.
51+ errBalanceTooLow = errors .New (
52+ "channel balance too low for loop out amount" ,
53+ )
4754)
4855
4956// swapClientServer implements the grpc service exposed by loopd.
@@ -89,7 +96,8 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
8996 }
9097
9198 sweepConfTarget , err := validateLoopOutRequest (
92- s .lnd .ChainParams , in .SweepConfTarget , sweepAddr , in .Label ,
99+ ctx , s .lnd .Client , s .lnd .ChainParams , in , sweepAddr ,
100+ s .impl .LoopOutMaxParts ,
93101 )
94102 if err != nil {
95103 return nil , err
@@ -981,9 +989,12 @@ func validateLoopInRequest(htlcConfTarget int32, external bool) (int32, error) {
981989}
982990
983991// validateLoopOutRequest validates the confirmation target, destination
984- // address and label of the loop out request.
985- func validateLoopOutRequest (chainParams * chaincfg.Params , confTarget int32 ,
986- sweepAddr btcutil.Address , label string ) (int32 , error ) {
992+ // address and label of the loop out request. It also checks that the requested
993+ // loop amount is valid given the available balance.
994+ func validateLoopOutRequest (ctx context.Context , lnd lndclient.LightningClient ,
995+ chainParams * chaincfg.Params , req * looprpc.LoopOutRequest ,
996+ sweepAddr btcutil.Address , maxParts uint32 ) (int32 , error ) {
997+
987998 // Check that the provided destination address has the correct format
988999 // for the active network.
9891000 if ! sweepAddr .IsForNet (chainParams ) {
@@ -992,9 +1003,101 @@ func validateLoopOutRequest(chainParams *chaincfg.Params, confTarget int32,
9921003 }
9931004
9941005 // Check that the label is valid.
995- if err := labels .Validate (label ); err != nil {
1006+ if err := labels .Validate (req .Label ); err != nil {
1007+ return 0 , err
1008+ }
1009+
1010+ channels , err := lnd .ListChannels (ctx )
1011+ if err != nil {
9961012 return 0 , err
9971013 }
9981014
999- return validateConfTarget (confTarget , loop .DefaultSweepConfTarget )
1015+ unlimitedChannels := len (req .OutgoingChanSet ) == 0
1016+ outgoingChanSetMap := make (map [uint64 ]bool )
1017+ for _ , chanID := range req .OutgoingChanSet {
1018+ outgoingChanSetMap [chanID ] = true
1019+ }
1020+
1021+ var activeChannelSet []lndclient.ChannelInfo
1022+ for _ , c := range channels {
1023+ // Don't bother looking at inactive channels.
1024+ if ! c .Active {
1025+ continue
1026+ }
1027+
1028+ // If no outgoing channel set was specified then all active
1029+ // channels are considered. However, if a channel set was
1030+ // specified then only the specified channels are considered.
1031+ if unlimitedChannels || outgoingChanSetMap [c .ChannelID ] {
1032+ activeChannelSet = append (activeChannelSet , c )
1033+ }
1034+ }
1035+
1036+ // Determine if the loop out request is theoretically possible given
1037+ // the amount requested, the maximum possible routing fees,
1038+ // the available channel set and the fact that equal splitting is
1039+ // used for MPP.
1040+ requiredBalance := btcutil .Amount (req .Amt + req .MaxSwapRoutingFee )
1041+ isRoutable , _ := hasBandwidth (activeChannelSet , requiredBalance ,
1042+ int (maxParts ))
1043+ if ! isRoutable {
1044+ return 0 , fmt .Errorf ("%w: Requested swap amount of %d " +
1045+ "sats along with the maximum routing fee of %d sats " +
1046+ "is more than what can be routed given current state " +
1047+ "of the channel set" , errBalanceTooLow , req .Amt ,
1048+ req .MaxSwapRoutingFee )
1049+ }
1050+
1051+ return validateConfTarget (
1052+ req .SweepConfTarget , loop .DefaultSweepConfTarget ,
1053+ )
1054+ }
1055+
1056+ // hasBandwidth simulates the MPP splitting logic that will be used by LND when
1057+ // attempting to route the payment. This function is used to evaluate if a
1058+ // payment will be routable given the splitting logic used by LND.
1059+ // It returns true if the amount is routable given the channel set and the
1060+ // maximum number of shards allowed. If the amount is routable then the number
1061+ // of shards used is also returned. This function makes an assumption that the
1062+ // minimum loop amount divided by max parts will not be less than the minimum
1063+ // shard amount. If the MPP logic changes, then this function should be updated.
1064+ func hasBandwidth (channels []lndclient.ChannelInfo , amt btcutil.Amount ,
1065+ maxParts int ) (bool , int ) {
1066+
1067+ scratch := make ([]btcutil.Amount , len (channels ))
1068+ var totalBandwidth btcutil.Amount
1069+ for i , channel := range channels {
1070+ scratch [i ] = channel .LocalBalance
1071+ totalBandwidth += channel .LocalBalance
1072+ }
1073+
1074+ if totalBandwidth < amt {
1075+ return false , 0
1076+ }
1077+
1078+ split := amt
1079+ for shard := 0 ; shard <= maxParts ; {
1080+ paid := false
1081+ for i := 0 ; i < len (scratch ); i ++ {
1082+ if scratch [i ] >= split {
1083+ scratch [i ] -= split
1084+ amt -= split
1085+ paid = true
1086+ shard ++
1087+ break
1088+ }
1089+ }
1090+
1091+ if amt == 0 {
1092+ return true , shard
1093+ }
1094+
1095+ if ! paid {
1096+ split /= 2
1097+ } else {
1098+ split = amt
1099+ }
1100+ }
1101+
1102+ return false , 0
10001103}
0 commit comments