Skip to content

Commit dcef032

Browse files
authored
Merge pull request #357 from ellemouton/validate-loop-out-amount
loopd: verify loop out amount
2 parents d6db618 + 65fe06c commit dcef032

File tree

4 files changed

+473
-50
lines changed

4 files changed

+473
-50
lines changed

client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func NewClient(dbDir string, cfg *ClientConfig) (*Client, func(), error) {
129129
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
130130
return time.NewTimer(d).C
131131
},
132+
LoopOutMaxParts: cfg.LoopOutMaxParts,
132133
}
133134

134135
sweeper := &sweep.Sweeper{

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ type clientConfig struct {
1515
Store loopdb.SwapStore
1616
LsatStore lsat.Store
1717
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time
18+
LoopOutMaxParts uint32
1819
}

loopd/swapclient_server.go

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ var (
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

Comments
 (0)