Skip to content

Commit f786aaa

Browse files
committed
loop: add support for the probe API
1 parent 7d044f5 commit f786aaa

20 files changed

+2116
-847
lines changed

client.go

Lines changed: 16 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,9 @@ 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+
)
564569
if err != nil {
565570
return nil, err
566571
}
@@ -625,3 +630,13 @@ func wrapGrpcError(message string, err error) error {
625630
grpcStatus.Message()),
626631
)
627632
}
633+
634+
// Probe asks the server to probe a route to us given a requested amount and
635+
// last hop. The server is free to discard frequent request to avoid abuse or if
636+
// there's been a recent probe to us for the same amount.
637+
func (s *Client) Probe(ctx context.Context, req *ProbeRequest) error {
638+
return s.Server.Probe(
639+
ctx, req.Amount, s.lndServices.NodePubkey, req.LastHop,
640+
req.RouteHints,
641+
)
642+
}

interface.go

Lines changed: 18 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,11 @@ 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
246252
}
247253

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

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: 82 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
)
@@ -495,6 +498,85 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
495498
}, nil
496499
}
497500

501+
// unmarshallRouteHints unmarshalls a list of route hints.
502+
func unmarshallRouteHints(rpcRouteHints []*looprpc.RouteHint) (
503+
[][]zpay32.HopHint, error) {
504+
505+
routeHints := make([][]zpay32.HopHint, 0, len(rpcRouteHints))
506+
for _, rpcRouteHint := range rpcRouteHints {
507+
routeHint := make(
508+
[]zpay32.HopHint, 0, len(rpcRouteHint.HopHints),
509+
)
510+
for _, rpcHint := range rpcRouteHint.HopHints {
511+
hint, err := unmarshallHopHint(rpcHint)
512+
if err != nil {
513+
return nil, err
514+
}
515+
516+
routeHint = append(routeHint, hint)
517+
}
518+
routeHints = append(routeHints, routeHint)
519+
}
520+
521+
return routeHints, nil
522+
}
523+
524+
// unmarshallHopHint unmarshalls a single hop hint.
525+
func unmarshallHopHint(rpcHint *looprpc.HopHint) (zpay32.HopHint, error) {
526+
pubBytes, err := hex.DecodeString(rpcHint.NodeId)
527+
if err != nil {
528+
return zpay32.HopHint{}, err
529+
}
530+
531+
pubkey, err := btcec.ParsePubKey(pubBytes, btcec.S256())
532+
if err != nil {
533+
return zpay32.HopHint{}, err
534+
}
535+
536+
return zpay32.HopHint{
537+
NodeID: pubkey,
538+
ChannelID: rpcHint.ChanId,
539+
FeeBaseMSat: rpcHint.FeeBaseMsat,
540+
FeeProportionalMillionths: rpcHint.FeeProportionalMillionths,
541+
CLTVExpiryDelta: uint16(rpcHint.CltvExpiryDelta),
542+
}, nil
543+
}
544+
545+
// Probe requests the server to probe the client's node to test inbound
546+
// liquidity.
547+
func (s *swapClientServer) Probe(ctx context.Context,
548+
req *looprpc.ProbeRequest) (*looprpc.ProbeResponse, error) {
549+
550+
log.Infof("Probe request received")
551+
552+
var lastHop *route.Vertex
553+
if req.LastHop != nil {
554+
lastHopVertex, err := route.NewVertexFromBytes(req.LastHop)
555+
if err != nil {
556+
return nil, err
557+
}
558+
559+
lastHop = &lastHopVertex
560+
}
561+
562+
routeHints, err := unmarshallRouteHints(req.RouteHints)
563+
if err != nil {
564+
return nil, err
565+
}
566+
567+
err = s.impl.Probe(ctx, &loop.ProbeRequest{
568+
Amount: btcutil.Amount(req.Amt),
569+
LastHop: lastHop,
570+
RouteHints: routeHints,
571+
})
572+
573+
if err != nil {
574+
return nil, err
575+
}
576+
577+
return &looprpc.ProbeResponse{}, nil
578+
}
579+
498580
func (s *swapClientServer) LoopIn(ctx context.Context,
499581
in *looprpc.LoopInRequest) (
500582
*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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
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
8585
// request that we send to the server.
86-
quote, err := cfg.server.GetLoopInQuote(globalCtx, request.Amount)
86+
quote, err := cfg.server.GetLoopInQuote(
87+
globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop,
88+
)
8789
if err != nil {
8890
return nil, wrapGrpcError("loop in terms", err)
8991
}

0 commit comments

Comments
 (0)