Skip to content

Commit

Permalink
loopd: quoting for static address loop-ins
Browse files Browse the repository at this point in the history
  • Loading branch information
hieblmi committed Aug 29, 2024
1 parent f1f7aca commit 1ac363b
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 20 deletions.
12 changes: 7 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,9 +646,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) {

Expand Down Expand Up @@ -694,7 +694,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
Expand All @@ -704,7 +704,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,
Expand Down
6 changes: 6 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 57 additions & 4 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
27 changes: 24 additions & 3 deletions loopd/swapclient_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,55 +145,76 @@ func TestValidateConfTarget(t *testing.T) {
func TestValidateLoopInRequest(t *testing.T) {
tests := []struct {
name string
amount int64
numDeposits uint32
external bool
confTarget int32
expectErr bool
expectedTarget int32
}{
{
name: "external and htlc conf set",
amount: 100_000,
external: true,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
name: "external and no conf",
amount: 100_000,
external: true,
confTarget: 0,
expectErr: false,
expectedTarget: 0,
},
{
name: "not external, zero conf",
amount: 100_000,
external: false,
confTarget: 0,
expectErr: false,
expectedTarget: loop.DefaultHtlcConfTarget,
},
{
name: "not external, bad conf",
amount: 100_000,
external: false,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
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 {
Expand Down
2 changes: 1 addition & 1 deletion loopin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion server_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 8 additions & 6 deletions swap_server_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit 1ac363b

Please sign in to comment.