Skip to content

Commit 91ad53a

Browse files
authored
Merge pull request #387 from bhandras/loop_in_probe
loop-in: allow clients to request server probes and extend loop-in quote with additional parameters for more accurate swap fees
2 parents 7d044f5 + bfb191c commit 91ad53a

23 files changed

+2218
-864
lines changed

client.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ var (
4848
// and pay for an LSAT token.
4949
globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout
5050

51+
// probeTimeout is the maximum time until a probe is allowed to take.
52+
probeTimeout = 3 * time.Minute
53+
5154
republishDelay = 10 * time.Second
5255

5356
// MinerFeeEstimationFailed is a magic number that is returned in a
@@ -560,7 +563,10 @@ func (s *Client) LoopInQuote(ctx context.Context,
560563
return nil, ErrSwapAmountTooHigh
561564
}
562565

563-
quote, err := s.Server.GetLoopInQuote(ctx, request.Amount)
566+
quote, err := s.Server.GetLoopInQuote(
567+
ctx, request.Amount, s.lndServices.NodePubkey, request.LastHop,
568+
request.RouteHints,
569+
)
564570
if err != nil {
565571
return nil, err
566572
}
@@ -625,3 +631,13 @@ func wrapGrpcError(message string, err error) error {
625631
grpcStatus.Message()),
626632
)
627633
}
634+
635+
// Probe asks the server to probe a route to us given a requested amount and
636+
// last hop. The server is free to discard frequent request to avoid abuse or if
637+
// there's been a recent probe to us for the same amount.
638+
func (s *Client) Probe(ctx context.Context, req *ProbeRequest) error {
639+
return s.Server.Probe(
640+
ctx, req.Amount, s.lndServices.NodePubkey, req.LastHop,
641+
req.RouteHints,
642+
)
643+
}

cmd/loop/loopin.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,25 @@ func loopIn(ctx *cli.Context) error {
109109
return err
110110
}
111111

112+
var lastHop []byte
113+
if ctx.IsSet(lastHopFlag.Name) {
114+
lastHopVertex, err := route.NewVertexFromStr(
115+
ctx.String(lastHopFlag.Name),
116+
)
117+
if err != nil {
118+
return err
119+
}
120+
121+
lastHop = lastHopVertex[:]
122+
}
123+
112124
quoteReq := &looprpc.QuoteRequest{
113-
Amt: int64(amt),
114-
ConfTarget: htlcConfTarget,
115-
ExternalHtlc: external,
125+
Amt: int64(amt),
126+
ConfTarget: htlcConfTarget,
127+
ExternalHtlc: external,
128+
LoopInLastHop: lastHop,
116129
}
130+
117131
quote, err := client.GetLoopInQuote(context.Background(), quoteReq)
118132
if err != nil {
119133
return err
@@ -147,17 +161,7 @@ func loopIn(ctx *cli.Context) error {
147161
HtlcConfTarget: htlcConfTarget,
148162
Label: label,
149163
Initiator: defaultInitiator,
150-
}
151-
152-
if ctx.IsSet(lastHopFlag.Name) {
153-
lastHop, err := route.NewVertexFromStr(
154-
ctx.String(lastHopFlag.Name),
155-
)
156-
if err != nil {
157-
return err
158-
}
159-
160-
req.LastHop = lastHop[:]
164+
LastHop: lastHop,
161165
}
162166

163167
resp, err := client.LoopIn(context.Background(), req)

cmd/loop/quote.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/lightninglabs/loop"
1010
"github.com/lightninglabs/loop/looprpc"
11+
"github.com/lightningnetwork/lnd/routing/route"
1112
"github.com/urfave/cli"
1213
)
1314

@@ -22,8 +23,16 @@ var quoteInCommand = cli.Command{
2223
Usage: "get a quote for the cost of a loop in swap",
2324
ArgsUsage: "amt",
2425
Description: "Allows to determine the cost of a swap up front",
25-
Flags: []cli.Flag{confTargetFlag, verboseFlag},
26-
Action: quoteIn,
26+
Flags: []cli.Flag{
27+
cli.StringFlag{
28+
Name: lastHopFlag.Name,
29+
Usage: "the pubkey of the last hop to use for the " +
30+
"quote",
31+
},
32+
confTargetFlag,
33+
verboseFlag,
34+
},
35+
Action: quoteIn,
2736
}
2837

2938
func quoteIn(ctx *cli.Context) error {
@@ -44,11 +53,23 @@ func quoteIn(ctx *cli.Context) error {
4453
}
4554
defer cleanup()
4655

47-
ctxb := context.Background()
4856
quoteReq := &looprpc.QuoteRequest{
4957
Amt: int64(amt),
5058
ConfTarget: int32(ctx.Uint64("conf_target")),
5159
}
60+
61+
if ctx.IsSet(lastHopFlag.Name) {
62+
lastHopVertex, err := route.NewVertexFromStr(
63+
ctx.String(lastHopFlag.Name),
64+
)
65+
if err != nil {
66+
return err
67+
}
68+
69+
quoteReq.LoopInLastHop = lastHopVertex[:]
70+
}
71+
72+
ctxb := context.Background()
5273
quoteResp, err := client.GetLoopInQuote(ctxb, quoteReq)
5374
if err != nil {
5475
return err

interface.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/lightninglabs/loop/swap"
99
"github.com/lightningnetwork/lnd/lntypes"
1010
"github.com/lightningnetwork/lnd/routing/route"
11+
"github.com/lightningnetwork/lnd/zpay32"
1112
)
1213

1314
// OutRequest contains the required parameters for a loop out swap.
@@ -243,6 +244,15 @@ type LoopInQuoteRequest struct {
243244
// ExternalHtlc specifies whether the htlc is published by an external
244245
// source.
245246
ExternalHtlc bool
247+
248+
// LastHop is an optional last hop to use. This last hop is used when
249+
// the client has already requested a server probe for more accurate
250+
// routing fee estimation.
251+
LastHop *route.Vertex
252+
253+
// RouteHints are optional route hints to reach the destination through
254+
// private channels.
255+
RouteHints [][]zpay32.HopHint
246256
}
247257

248258
// LoopInQuote contains estimates for the fees making up the total swap cost
@@ -340,3 +350,15 @@ func (s *In) LastUpdate() time.Time {
340350
func (s *In) SwapHash() lntypes.Hash {
341351
return s.Hash
342352
}
353+
354+
// ProbeRequest specifies probe parameters for the server probe.
355+
type ProbeRequest struct {
356+
// Amount is the amount that will be probed.
357+
Amount btcutil.Amount
358+
359+
// LastHop is the last hop along the route.
360+
LastHop *route.Vertex
361+
362+
// Optional hop hints.
363+
RouteHints [][]zpay32.HopHint
364+
}

loopd/macaroons.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ var (
9595
Entity: "suggestions",
9696
Action: "write",
9797
}},
98+
"/looprpc.SwapClient/Probe": {{
99+
Entity: "swap",
100+
Action: "execute",
101+
}, {
102+
Entity: "loop",
103+
Action: "in",
104+
}},
98105
}
99106

100107
// allPermissions is the list of all existing permissions that exist

loopd/swapclient_server.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package loopd
22

33
import (
44
"context"
5+
"encoding/hex"
56
"errors"
67
"fmt"
78
"sort"
89
"sync"
910
"time"
1011

12+
"github.com/btcsuite/btcd/btcec"
1113
"github.com/btcsuite/btcd/chaincfg"
1214
"github.com/btcsuite/btcutil"
1315
"github.com/lightninglabs/lndclient"
@@ -22,6 +24,7 @@ import (
2224
"github.com/lightningnetwork/lnd/lnwire"
2325
"github.com/lightningnetwork/lnd/queue"
2426
"github.com/lightningnetwork/lnd/routing/route"
27+
"github.com/lightningnetwork/lnd/zpay32"
2528
"google.golang.org/grpc/codes"
2629
"google.golang.org/grpc/status"
2730
)
@@ -480,10 +483,29 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
480483
return nil, err
481484
}
482485

486+
var lastHop *route.Vertex
487+
if req.LoopInLastHop != nil {
488+
lastHopVertex, err := route.NewVertexFromBytes(
489+
req.LoopInLastHop,
490+
)
491+
if err != nil {
492+
return nil, err
493+
}
494+
495+
lastHop = &lastHopVertex
496+
}
497+
498+
routeHints, err := unmarshallRouteHints(req.LoopInRouteHints)
499+
if err != nil {
500+
return nil, err
501+
}
502+
483503
quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
484504
Amount: btcutil.Amount(req.Amt),
485505
HtlcConfTarget: htlcConfTarget,
486506
ExternalHtlc: req.ExternalHtlc,
507+
LastHop: lastHop,
508+
RouteHints: routeHints,
487509
})
488510
if err != nil {
489511
return nil, err
@@ -495,6 +517,84 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
495517
}, nil
496518
}
497519

520+
// unmarshallRouteHints unmarshalls a list of route hints.
521+
func unmarshallRouteHints(rpcRouteHints []*looprpc.RouteHint) (
522+
[][]zpay32.HopHint, error) {
523+
524+
routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
525+
for _, rpcRouteHint := range rpcRouteHints {
526+
routeHint := make(
527+
[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
528+
)
529+
for _, rpcHint := range rpcRouteHint.HopHints {
530+
hint, err := unmarshallHopHint(rpcHint)
531+
if err != nil {
532+
return nil, err
533+
}
534+
535+
routeHint = append(routeHint, hint)
536+
}
537+
routeHints = append(routeHints, routeHint)
538+
}
539+
540+
return routeHints, nil
541+
}
542+
543+
// unmarshallHopHint unmarshalls a single hop hint.
544+
func unmarshallHopHint(rpcHint *looprpc.HopHint) (zpay32.HopHint, error) {
545+
pubBytes, err := hex.DecodeString(rpcHint.NodeId)
546+
if err != nil {
547+
return zpay32.HopHint{}, err
548+
}
549+
550+
pubkey, err := btcec.ParsePubKey(pubBytes, btcec.S256())
551+
if err != nil {
552+
return zpay32.HopHint{}, err
553+
}
554+
555+
return zpay32.HopHint{
556+
NodeID: pubkey,
557+
ChannelID: rpcHint.ChanId,
558+
FeeBaseMSat: rpcHint.FeeBaseMsat,
559+
FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
560+
CLTVExpiryDelta: uint16(rpcHint.CltvExpiryDelta),
561+
}, nil
562+
}
563+
564+
// Probe requests the server to probe the client's node to test inbound
565+
// liquidity.
566+
func (s *swapClientServer) Probe(ctx context.Context,
567+
req *looprpc.ProbeRequest) (*looprpc.ProbeResponse, error) {
568+
569+
log.Infof("Probe request received")
570+
571+
var lastHop *route.Vertex
572+
if req.LastHop != nil {
573+
lastHopVertex, err := route.NewVertexFromBytes(req.LastHop)
574+
if err != nil {
575+
return nil, err
576+
}
577+
578+
lastHop = &lastHopVertex
579+
}
580+
581+
routeHints, err := unmarshallRouteHints(req.RouteHints)
582+
if err != nil {
583+
return nil, err
584+
}
585+
586+
err = s.impl.Probe(ctx, &loop.ProbeRequest{
587+
Amount: btcutil.Amount(req.Amt),
588+
LastHop: lastHop,
589+
RouteHints: routeHints,
590+
})
591+
if err != nil {
592+
return nil, err
593+
}
594+
595+
return &looprpc.ProbeResponse{}, nil
596+
}
597+
498598
func (s *swapClientServer) LoopIn(ctx context.Context,
499599
in *looprpc.LoopInRequest) (
500600
*looprpc.SwapResponse, error) {

loopdb/protocol_version.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ const (
4343
// canceling loop out swaps.
4444
ProtocolVersionLoopOutCancel = 7
4545

46+
// ProtocolVerionProbe indicates that the client is able to request
47+
// the server to perform a probe to test inbound liquidty.
48+
ProtocolVersionProbe ProtocolVersion = 8
49+
4650
// ProtocolVersionUnrecorded is set for swaps were created before we
4751
// started saving protocol version with swaps.
4852
ProtocolVersionUnrecorded ProtocolVersion = math.MaxUint32
4953

5054
// CurrentRPCProtocolVersion defines the version of the RPC protocol
5155
// that is currently supported by the loop client.
52-
CurrentRPCProtocolVersion = looprpc.ProtocolVersion_LOOP_OUT_CANCEL
56+
CurrentRPCProtocolVersion = looprpc.ProtocolVersion_PROBE
5357

5458
// CurrentInternalProtocolVersion defines the RPC current protocol in
5559
// the internal representation.
@@ -88,6 +92,9 @@ func (p ProtocolVersion) String() string {
8892
case ProtocolVersionLoopOutCancel:
8993
return "Loop Out Cancel"
9094

95+
case ProtocolVersionProbe:
96+
return "Probe"
97+
9198
default:
9299
return "Unknown"
93100
}

loopdb/protocol_version_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func TestProtocolVersionSanity(t *testing.T) {
2222
ProtocolVersionHtlcV2,
2323
ProtocolVersionMultiLoopIn,
2424
ProtocolVersionLoopOutCancel,
25+
ProtocolVersionProbe,
2526
}
2627

2728
rpcVersions := [...]looprpc.ProtocolVersion{
@@ -33,6 +34,7 @@ func TestProtocolVersionSanity(t *testing.T) {
3334
looprpc.ProtocolVersion_HTLC_V2,
3435
looprpc.ProtocolVersion_MULTI_LOOP_IN,
3536
looprpc.ProtocolVersion_LOOP_OUT_CANCEL,
37+
looprpc.ProtocolVersion_PROBE,
3638
}
3739

3840
require.Equal(t, len(versions), len(rpcVersions))

loopin.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,15 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
8282

8383
// Request current server loop in terms and use these to calculate the
8484
// swap fee that we should subtract from the swap amount in the payment
85-
// request that we send to the server.
86-
quote, err := cfg.server.GetLoopInQuote(globalCtx, request.Amount)
85+
// request that we send to the server. We pass nil as optional route
86+
// hints as hop hint selection when generating invoices with private
87+
// channels is an LND side black box feaure. Advanced users will quote
88+
// directly anyway and there they have the option to add specific
89+
// route hints.
90+
quote, err := cfg.server.GetLoopInQuote(
91+
globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop,
92+
nil,
93+
)
8794
if err != nil {
8895
return nil, wrapGrpcError("loop in terms", err)
8996
}

0 commit comments

Comments
 (0)