diff --git a/cmd/loop/quote.go b/cmd/loop/quote.go index db23304c1..b467e5ce7 100644 --- a/cmd/loop/quote.go +++ b/cmd/loop/quote.go @@ -229,23 +229,22 @@ func printQuoteInResp(req *looprpc.QuoteRequest, resp *looprpc.InQuoteResponse, verbose bool) { totalFee := resp.HtlcPublishFeeSat + resp.SwapFeeSat + amt := req.Amt + if amt == 0 { + amt = resp.QuotedAmt + } if req.DepositOutpoints != nil { - if req.Amt == 0 { - fmt.Printf(satAmtFmt, "Previously deposited "+ - "on-chain:", resp.QuotedAmt) - } else { - fmt.Printf(satAmtFmt, "Previously deposited "+ - "on-chain:", req.Amt) - } + fmt.Printf(satAmtFmt, "Previously deposited on-chain:", + amt) } else { - fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt) + fmt.Printf(satAmtFmt, "Send on-chain:", amt) } - fmt.Printf(satAmtFmt, "Receive off-chain:", req.Amt-totalFee) + fmt.Printf(satAmtFmt, "Receive off-chain:", amt-totalFee) switch { case req.ExternalHtlc && !verbose: - // If it's external then we don't know the miner fee hence the + // If it's external, then we don't know the miner fee hence the // total cost. fmt.Printf(satAmtFmt, "Loop service fee:", resp.SwapFeeSat) diff --git a/staticaddr/loopin/interface.go b/staticaddr/loopin/interface.go index c4582730c..1bf32235a 100644 --- a/staticaddr/loopin/interface.go +++ b/staticaddr/loopin/interface.go @@ -15,6 +15,17 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) +const ( + // DefaultLoopInOnChainCltvDelta is the time lock relative to current + // block height that swap server will accept on the swap initiation + // call. + DefaultLoopInOnChainCltvDelta = 1000 + + // DepositHtlcDelta is a safety buffer of blocks that needs to exist + // between the deposit expiry height and the htlc expiry height. + DepositHtlcDelta = 50 +) + type ( // ValidateLoopInContract validates the contract parameters against our // request. diff --git a/staticaddr/loopin/manager.go b/staticaddr/loopin/manager.go index 478b85f41..05f28f1c9 100644 --- a/staticaddr/loopin/manager.go +++ b/staticaddr/loopin/manager.go @@ -862,8 +862,25 @@ func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn, // are needed to cover the amount requested without leaving a dust change. It // returns an error if the sum of deposits minus dust is less than the requested // amount. -func SelectDeposits(targetAmount btcutil.Amount, deposits []*deposit.Deposit, - csvExpiry uint32, blockHeight uint32) ([]*deposit.Deposit, error) { +func SelectDeposits(targetAmount btcutil.Amount, + unfilteredDeposits []*deposit.Deposit, csvExpiry uint32, + blockHeight uint32) ([]*deposit.Deposit, error) { + + // Filter out deposits that are too close to expiry to be swapped. + var deposits []*deposit.Deposit + for _, d := range unfilteredDeposits { + if !IsSwappable( + uint32(d.ConfirmationHeight), blockHeight, csvExpiry, + ) { + + log.Debugf("Skipping deposit %s as it expires before "+ + "the htlc", d.OutPoint.String()) + + continue + } + + deposits = append(deposits, d) + } // Sort the deposits by amount in descending order, then by // blocks-until-expiry in ascending order. @@ -901,6 +918,25 @@ func SelectDeposits(targetAmount btcutil.Amount, deposits []*deposit.Deposit, selectedAmount, targetAmount) } +// IsSwappable checks if a deposit is swappable. It returns true if the deposit +// is not expired and the htlc is not too close to expiry. +func IsSwappable(confirmationHeight, blockHeight, csvExpiry uint32) bool { + // The deposit expiry height is the confirmation height plus the csv + // expiry. + depositExpiryHeight := confirmationHeight + csvExpiry + + // The htlc expiry height is the current height plus the htlc + // cltv delta. + htlcExpiryHeight := blockHeight + DefaultLoopInOnChainCltvDelta + + // Ensure that the deposit doesn't expire before the htlc. + if depositExpiryHeight < htlcExpiryHeight+DepositHtlcDelta { + return false + } + + return true +} + // DeduceSwapAmount calculates the swap amount based on the selected amount and // the total deposit amount. It checks if the selected amount leaves a dust // change output or exceeds the total deposits value. Note that if the selected diff --git a/staticaddr/loopin/manager_test.go b/staticaddr/loopin/manager_test.go index d922cdbbc..d908a9e16 100644 --- a/staticaddr/loopin/manager_test.go +++ b/staticaddr/loopin/manager_test.go @@ -29,16 +29,16 @@ type testCase struct { func TestSelectDeposits(t *testing.T) { d1, d2, d3, d4 := &deposit.Deposit{ Value: 1_000_000, - ConfirmationHeight: 1000, + ConfirmationHeight: 5_000, }, &deposit.Deposit{ Value: 2_000_000, - ConfirmationHeight: 2000, + ConfirmationHeight: 5_001, }, &deposit.Deposit{ Value: 3_000_000, - ConfirmationHeight: 3000, + ConfirmationHeight: 5_002, }, &deposit.Deposit{ Value: 3_000_000, - ConfirmationHeight: 3001, + ConfirmationHeight: 5_003, } d1.Hash = chainhash.Hash{1} d1.Index = 0 @@ -121,6 +121,43 @@ func TestSelectDeposits(t *testing.T) { expected: []*deposit.Deposit{d3}, expectedErr: "", }, + { + name: "prefilter filters deposits close to expiry", + deposits: func() []*deposit.Deposit { + // dClose expires before + // htlcExpiry+DepositHtlcDelta and must be + // filtered out. dOK expires exactly at the + // threshold and must be eligible. + dClose := &deposit.Deposit{ + Value: 3_000_000, + ConfirmationHeight: 3000, + } + dClose.Hash = chainhash.Hash{5} + dClose.Index = 0 + dOK := &deposit.Deposit{ + Value: 2_000_000, + ConfirmationHeight: 3050, + } + dOK.Hash = chainhash.Hash{6} + dOK.Index = 0 + return []*deposit.Deposit{dClose, dOK} + }(), + targetValue: 1_000_000, + csvExpiry: 1000, + blockHeight: 3000, + expected: func() []*deposit.Deposit { + // Only dOK should be considered. + // dClose is filtered. + dOK := &deposit.Deposit{ + Value: 2_000_000, + ConfirmationHeight: 3050, + } + dOK.Hash = chainhash.Hash{6} + dOK.Index = 0 + return []*deposit.Deposit{dOK} + }(), + expectedErr: "", + }, } for _, tc := range testCases {