From c4062bd0423a731836bf6d02b85cc9a6220ea321 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Thu, 6 Jun 2024 15:30:50 +0200 Subject: [PATCH] loopd: quoting for static address loop-ins --- client.go | 12 ++++--- interface.go | 6 ++++ loopd/swapclient_server.go | 61 ++++++++++++++++++++++++++++++--- loopd/swapclient_server_test.go | 27 +++++++++++++-- loopin.go | 2 +- server_mock_test.go | 3 +- swap_server_client.go | 14 ++++---- 7 files changed, 105 insertions(+), 20 deletions(-) diff --git a/client.go b/client.go index 504fcb672..7142549c7 100644 --- a/client.go +++ b/client.go @@ -645,9 +645,9 @@ func (s *Client) LoopIn(globalCtx context.Context, return swapInfo, nil } -// LoopInQuote takes an amount and returns a break down of estimated -// costs for the client. Both the swap server and the on-chain fee estimator are -// queried to get to build the quote response. +// LoopInQuote takes an amount and returns a breakdown of estimated costs for +// the client. Both the swap server and the on-chain fee estimator are queried +// to get to build the quote response. func (s *Client) LoopInQuote(ctx context.Context, request *LoopInQuoteRequest) (*LoopInQuote, error) { @@ -693,7 +693,7 @@ func (s *Client) LoopInQuote(ctx context.Context, quote, err := s.Server.GetLoopInQuote( ctx, request.Amount, s.lndServices.NodePubkey, request.LastHop, - request.RouteHints, request.Initiator, + request.RouteHints, request.Initiator, request.NumDeposits, ) if err != nil { return nil, err @@ -703,7 +703,9 @@ func (s *Client) LoopInQuote(ctx context.Context, // We don't calculate the on-chain fee if the HTLC is going to be // published externally. - if request.ExternalHtlc { + // We also don't calculate the on-chain fee if the loop in is funded by + // static address deposits because we don't publish the HTLC on-chain. + if request.ExternalHtlc || request.NumDeposits > 0 { return &LoopInQuote{ SwapFee: swapFee, MinerFee: 0, diff --git a/interface.go b/interface.go index 1f13aea52..37e5e01c4 100644 --- a/interface.go +++ b/interface.go @@ -288,6 +288,12 @@ type LoopInQuoteRequest struct { // initiated the swap (loop CLI, autolooper, LiT UI and so on) and is // appended to the user agent string. Initiator string + + // The number of static address deposits the client wants to quote for. + // If the number of deposits exceeds one the server will apply a + // per-input service fee. This is to cover for the increased on-chain + // fee the server has to pay when the sweeping transaction is broadcast. + NumDeposits uint32 } // LoopInQuote contains estimates for the fees making up the total swap cost diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 660a0c1e3..7d511969a 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -748,13 +748,53 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context, log.Infof("Loop in quote request received") + var ( + numDeposits = uint32(len(req.DepositOutpoints)) + err error + ) + htlcConfTarget, err := validateLoopInRequest( - req.ConfTarget, req.ExternalHtlc, + req.ConfTarget, req.ExternalHtlc, numDeposits, req.Amt, ) if err != nil { return nil, err } + // Retrieve deposits to calculate their total value. + var summary *clientrpc.StaticAddressSummaryResponse + amount := btcutil.Amount(req.Amt) + if len(req.DepositOutpoints) > 0 { + summary, err = s.GetStaticAddressSummary( + ctx, &clientrpc.StaticAddressSummaryRequest{ + Outpoints: req.DepositOutpoints, + }, + ) + if err != nil { + return nil, err + } + + if summary == nil { + return nil, fmt.Errorf("no summary returned for " + + "deposit outpoints") + } + + // The requested amount should be 0 here if the request + // contained deposit outpoints. + if amount != 0 && len(summary.FilteredDeposits) > 0 { + return nil, fmt.Errorf("amount should be 0 for " + + "deposit quotes") + } + + // In case we quote for deposits we send the server both the + // total value and the number of deposits. This is so the server + // can probe the total amount and calculate the per input fee. + if amount == 0 && len(summary.FilteredDeposits) > 0 { + for _, deposit := range summary.FilteredDeposits { + amount += btcutil.Amount(deposit.Value) + } + } + } + var ( routeHints [][]zpay32.HopHint lastHop *route.Vertex @@ -778,13 +818,14 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context, } quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{ - Amount: btcutil.Amount(req.Amt), + Amount: amount, HtlcConfTarget: htlcConfTarget, ExternalHtlc: req.ExternalHtlc, LastHop: lastHop, RouteHints: routeHints, Private: req.Private, Initiator: defaultLoopdInitiator, + NumDeposits: numDeposits, }) if err != nil { return nil, err @@ -881,7 +922,7 @@ func (s *swapClientServer) LoopIn(ctx context.Context, log.Infof("Loop in request received") htlcConfTarget, err := validateLoopInRequest( - in.HtlcConfTarget, in.ExternalHtlc, + in.HtlcConfTarget, in.ExternalHtlc, 0, in.Amt, ) if err != nil { return nil, err @@ -1710,7 +1751,13 @@ func validateConfTarget(target, defaultTarget int32) (int32, error) { // validateLoopInRequest fails if the mutually exclusive conf target and // external parameters are both set. -func validateLoopInRequest(htlcConfTarget int32, external bool) (int32, error) { +func validateLoopInRequest(htlcConfTarget int32, external bool, + numDeposits uint32, amount int64) (int32, error) { + + if amount == 0 && numDeposits == 0 { + return 0, errors.New("either amount or deposits must be set") + } + // If the htlc is going to be externally set, the htlcConfTarget should // not be set, because it has no relevance when the htlc is external. if external && htlcConfTarget != 0 { @@ -1724,6 +1771,12 @@ func validateLoopInRequest(htlcConfTarget int32, external bool) (int32, error) { return 0, nil } + // If the loop in uses static address deposits, we do not need to set a + // confirmation target since the HTLC won't be published by the client. + if numDeposits > 0 { + return 0, nil + } + return validateConfTarget(htlcConfTarget, loop.DefaultHtlcConfTarget) } diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index 585d87f31..fae2276ca 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -145,6 +145,8 @@ func TestValidateConfTarget(t *testing.T) { func TestValidateLoopInRequest(t *testing.T) { tests := []struct { name string + amount int64 + numDeposits uint32 external bool confTarget int32 expectErr bool @@ -152,6 +154,7 @@ func TestValidateLoopInRequest(t *testing.T) { }{ { name: "external and htlc conf set", + amount: 100_000, external: true, confTarget: 1, expectErr: true, @@ -159,6 +162,7 @@ func TestValidateLoopInRequest(t *testing.T) { }, { name: "external and no conf", + amount: 100_000, external: true, confTarget: 0, expectErr: false, @@ -166,6 +170,7 @@ func TestValidateLoopInRequest(t *testing.T) { }, { name: "not external, zero conf", + amount: 100_000, external: false, confTarget: 0, expectErr: false, @@ -173,6 +178,7 @@ func TestValidateLoopInRequest(t *testing.T) { }, { name: "not external, bad conf", + amount: 100_000, external: false, confTarget: 1, expectErr: true, @@ -180,20 +186,35 @@ func TestValidateLoopInRequest(t *testing.T) { }, { name: "not external, ok conf", + amount: 100_000, external: false, confTarget: 5, expectErr: false, expectedTarget: 5, }, + { + name: "not external, amount no deposit", + amount: 100_000, + numDeposits: 0, + external: false, + expectErr: false, + expectedTarget: loop.DefaultHtlcConfTarget, + }, + { + name: "not external, deposit no amount", + amount: 100_000, + numDeposits: 1, + external: false, + expectErr: false, + }, } for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { external := test.external conf, err := validateLoopInRequest( - test.confTarget, external, + test.confTarget, external, test.numDeposits, + test.amount, ) if test.expectErr { diff --git a/loopin.go b/loopin.go index b72d1795f..21480a1ac 100644 --- a/loopin.go +++ b/loopin.go @@ -128,7 +128,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig, // hints. quote, err := cfg.server.GetLoopInQuote( globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop, - request.RouteHints, request.Initiator, + request.RouteHints, request.Initiator, 0, ) if err != nil { return nil, wrapGrpcError("loop in terms", err) diff --git a/server_mock_test.go b/server_mock_test.go index f46a49774..d10223e7c 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -225,7 +225,8 @@ func (s *serverMock) GetLoopInTerms(ctx context.Context, initiator string) ( } func (s *serverMock) GetLoopInQuote(context.Context, btcutil.Amount, - route.Vertex, *route.Vertex, [][]zpay32.HopHint, string) (*LoopInQuote, error) { + route.Vertex, *route.Vertex, [][]zpay32.HopHint, string, + uint32) (*LoopInQuote, error) { return &LoopInQuote{ SwapFee: testSwapFee, diff --git a/swap_server_client.go b/swap_server_client.go index a40aa3a3b..c2ece0b81 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -82,7 +82,7 @@ type swapServerClient interface { GetLoopInQuote(ctx context.Context, amt btcutil.Amount, pubKey route.Vertex, lastHop *route.Vertex, routeHints [][]zpay32.HopHint, - initiator string) (*LoopInQuote, error) + initiator string, numDeposits uint32) (*LoopInQuote, error) Probe(ctx context.Context, amt btcutil.Amount, target route.Vertex, lastHop *route.Vertex, routeHints [][]zpay32.HopHint) error @@ -268,7 +268,8 @@ func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context, func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context, amt btcutil.Amount, pubKey route.Vertex, lastHop *route.Vertex, - routeHints [][]zpay32.HopHint, initiator string) (*LoopInQuote, error) { + routeHints [][]zpay32.HopHint, initiator string, + numDeposits uint32) (*LoopInQuote, error) { err := s.Probe(ctx, amt, pubKey, lastHop, routeHints) if err != nil && status.Code(err) != codes.Unavailable { @@ -279,10 +280,11 @@ func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context, defer rpcCancel() req := &looprpc.ServerLoopInQuoteRequest{ - Amt: uint64(amt), - ProtocolVersion: loopdb.CurrentRPCProtocolVersion(), - Pubkey: pubKey[:], - UserAgent: UserAgent(initiator), + Amt: uint64(amt), + ProtocolVersion: loopdb.CurrentRPCProtocolVersion(), + Pubkey: pubKey[:], + UserAgent: UserAgent(initiator), + NumStaticAddressDeposits: numDeposits, } if lastHop != nil {