From 9f25fdd0bb5f5bb952bd55a10b6baeb34239e840 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 30 Sep 2019 20:11:32 -0400 Subject: [PATCH 1/3] store: use correct vars for loop in assertions --- store_mock_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/store_mock_test.go b/store_mock_test.go index 70c53dd..ea28dda 100644 --- a/store_mock_test.go +++ b/store_mock_test.go @@ -170,8 +170,8 @@ func (s *storeMock) UpdateLoopIn(hash lntypes.Hash, time time.Time, } updates = append(updates, state) - s.loopOutUpdates[hash] = updates - s.loopOutUpdateChan <- state + s.loopInUpdates[hash] = updates + s.loopInUpdateChan <- state return nil } @@ -214,9 +214,9 @@ func (s *storeMock) assertLoopInStored() { func (s *storeMock) assertLoopInState(expectedState loopdb.SwapState) { s.t.Helper() - state := <-s.loopOutUpdateChan + state := <-s.loopInUpdateChan if state.State != expectedState { - s.t.Fatalf("unexpected state") + s.t.Fatalf("expected state %v, got %v", expectedState, state) } } From 09029bfdec15e1b9e2e1a56329ad9e789c556394 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 1 Oct 2019 11:21:17 -0400 Subject: [PATCH 2/3] test: allow custom fee estimates --- test/lnd_services_mock.go | 16 ++++++++++++---- test/walletkit_mock.go | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index b085c4f..c7a7fa7 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -6,12 +6,12 @@ import ( "time" "github.com/btcsuite/btcd/chaincfg" - "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/zpay32" - "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/zpay32" ) var testStartingHeight = int32(600) @@ -20,7 +20,9 @@ var testStartingHeight = int32(600) // tests. func NewMockLnd() *LndMockServices { lightningClient := &mockLightningClient{} - walletKit := &mockWalletKit{} + walletKit := &mockWalletKit{ + feeEstimates: make(map[int32]lnwallet.SatPerKWeight), + } chainNotifier := &mockChainNotifier{} signer := &mockSigner{} invoices := &mockInvoices{} @@ -197,3 +199,9 @@ func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice, return zpay32.Decode(request, s.ChainParams) } + +func (s *LndMockServices) SetFeeEstimate(confTarget int32, + feeEstimate lnwallet.SatPerKWeight) { + + s.WalletKit.(*mockWalletKit).feeEstimates[confTarget] = feeEstimate +} diff --git a/test/walletkit_mock.go b/test/walletkit_mock.go index c8dc5bb..8ebc57f 100644 --- a/test/walletkit_mock.go +++ b/test/walletkit_mock.go @@ -8,15 +8,19 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" ) type mockWalletKit struct { - lnd *LndMockServices - keyIndex int32 + lnd *LndMockServices + keyIndex int32 + feeEstimates map[int32]lnwallet.SatPerKWeight } +var _ lndclient.WalletKitClient = (*mockWalletKit)(nil) + func (m *mockWalletKit) DeriveNextKey(ctx context.Context, family int32) ( *keychain.KeyDescriptor, error) { @@ -87,9 +91,15 @@ func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut, func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) ( lnwallet.SatPerKWeight, error) { + if confTarget <= 1 { return 0, errors.New("conf target must be greater than 1") } - return 10000, nil + feeEstimate, ok := m.feeEstimates[confTarget] + if !ok { + return 10000, nil + } + + return feeEstimate, nil } From e0d23cb1800430a6926d4de5595df31720ec7e57 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 1 Oct 2019 11:21:19 -0400 Subject: [PATCH 3/3] loopout: compare delta from htlc expiry correctly This addresses an issue where using a sweep confirmation target greater than the default would result in most cases not revealing the preimage due to the default confirmation target yielding a higher fee than the max miner fee backed by the confirmation target provided. --- loopout.go | 2 +- loopout_test.go | 166 +++++++++++++++++++++++++++++++++++++++++++++ store_mock_test.go | 9 +++ 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/loopout.go b/loopout.go index eb46d63..801abbd 100644 --- a/loopout.go +++ b/loopout.go @@ -597,7 +597,7 @@ func (s *loopOutSwap) sweep(ctx context.Context, // close to the expiration height, in which case we'll use the default // if it is better than what the client provided. confTarget := s.SweepConfTarget - if s.CltvExpiry-s.height >= DefaultSweepConfTargetDelta && + if s.CltvExpiry-s.height <= DefaultSweepConfTargetDelta && confTarget > DefaultSweepConfTarget { confTarget = DefaultSweepConfTarget } diff --git a/loopout_test.go b/loopout_test.go index 97493a6..c491e26 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -6,6 +6,9 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" @@ -99,3 +102,166 @@ func TestLateHtlcPublish(t *testing.T) { t.Fatal(err) } } + +// TestCustomSweepConfTarget ensures we are able to sweep a Loop Out HTLC with a +// custom confirmation target. +func TestCustomSweepConfTarget(t *testing.T) { + defer test.Guard(t)() + + lnd := test.NewMockLnd() + ctx := test.NewContext(t, lnd) + + // Use the highest sweep confirmation target before we attempt to use + // the default. + testRequest.SweepConfTarget = testLoopOutOnChainCltvDelta - + DefaultSweepConfTargetDelta - 1 + + // Set up custom fee estimates such that the lower confirmation target + // yields a much higher fee rate. + ctx.Lnd.SetFeeEstimate(testRequest.SweepConfTarget, 250) + ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000) + + cfg := &swapConfig{ + lnd: &lnd.LndServices, + store: newStoreMock(t), + server: newServerMock(), + } + swap, err := newLoopOutSwap( + context.Background(), cfg, ctx.Lnd.Height, testRequest, + ) + if err != nil { + t.Fatal(err) + } + + // Set up the required dependencies to execute the swap. + // + // TODO: create test context similar to loopInTestContext. + sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices} + blockEpochChan := make(chan interface{}) + statusChan := make(chan SwapInfo) + expiryChan := make(chan time.Time) + timerFactory := func(expiry time.Duration) <-chan time.Time { + return expiryChan + } + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), &executeConfig{ + statusChan: statusChan, + blockEpochChan: blockEpochChan, + timerFactory: timerFactory, + sweeper: sweeper, + }, ctx.Lnd.Height) + if err != nil { + logger.Error(err) + } + errChan <- err + }() + + // The swap should be found in its initial state. + cfg.store.(*storeMock).assertLoopOutStored() + state := <-statusChan + if state.State != loopdb.StateInitiated { + t.Fatal("unexpected state") + } + + // We'll then pay both the swap and prepay invoice, which should trigger + // the server to publish the on-chain HTLC. + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + signalSwapPaymentResult(nil) + signalPrepaymentResult(nil) + + // Notify the confirmation notification for the HTLC. + ctx.AssertRegisterConf() + + blockEpochChan <- int32(ctx.Lnd.Height + 1) + + htlcTx := wire.NewMsgTx(2) + htlcTx.AddTxOut(&wire.TxOut{ + Value: int64(swap.AmountRequested), + PkScript: swap.htlc.PkScript, + }) + + ctx.NotifyConf(htlcTx) + + // The client should then register for a spend of the HTLC and attempt + // to sweep it using the custom confirmation target. + ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript) + + expiryChan <- time.Now() + + cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) + status := <-statusChan + if status.State != loopdb.StatePreimageRevealed { + t.Fatalf("expected state %v, got %v", + loopdb.StatePreimageRevealed, status.State) + } + + // assertSweepTx performs some sanity checks on a sweep transaction to + // ensure it was constructed correctly. + assertSweepTx := func(expConfTarget int32) *wire.MsgTx { + t.Helper() + + sweepTx := ctx.ReceiveTx() + if sweepTx.TxIn[0].PreviousOutPoint.Hash != htlcTx.TxHash() { + t.Fatalf("expected sweep tx to spend %v, got %v", + htlcTx.TxHash(), sweepTx.TxIn[0].PreviousOutPoint) + } + + // The fee used for the sweep transaction is an estimate based + // on the maximum witness size, so we should expect to see a + // lower fee when using the actual witness size of the + // transaction. + fee := btcutil.Amount( + htlcTx.TxOut[0].Value - sweepTx.TxOut[0].Value, + ) + + weight := blockchain.GetTransactionWeight(btcutil.NewTx(sweepTx)) + feeRate, err := ctx.Lnd.WalletKit.EstimateFee( + context.Background(), expConfTarget, + ) + if err != nil { + t.Fatalf("unable to retrieve fee estimate: %v", err) + } + minFee := feeRate.FeeForWeight(weight) + maxFee := btcutil.Amount(float64(minFee) * 1.1) + + if fee < minFee && fee > maxFee { + t.Fatalf("expected sweep tx to have fee between %v-%v, "+ + "got %v", minFee, maxFee, fee) + } + + return sweepTx + } + + // The sweep should have a fee that corresponds to the custom + // confirmation target. + sweepTx := assertSweepTx(testRequest.SweepConfTarget) + + // We'll then notify the height at which we begin using the default + // confirmation target. + defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta - + DefaultSweepConfTargetDelta + blockEpochChan <- int32(defaultConfTargetHeight) + expiryChan <- time.Now() + + // We should expect to see another sweep using the higher fee since the + // spend hasn't been confirmed yet. + sweepTx = assertSweepTx(DefaultSweepConfTarget) + + // Notify the spend so that the swap reaches its final state. + ctx.NotifySpend(sweepTx, 0) + + cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) + status = <-statusChan + if status.State != loopdb.StateSuccess { + t.Fatalf("expected state %v, got %v", loopdb.StateSuccess, + status.State) + } + + if err := <-errChan; err != nil { + t.Fatal(err) + } +} diff --git a/store_mock_test.go b/store_mock_test.go index ea28dda..317c864 100644 --- a/store_mock_test.go +++ b/store_mock_test.go @@ -205,6 +205,15 @@ func (s *storeMock) assertLoopOutStored() { } } +func (s *storeMock) assertLoopOutState(expectedState loopdb.SwapState) { + s.t.Helper() + + state := <-s.loopOutUpdateChan + if state.State != expectedState { + s.t.Fatalf("expected state %v, got %v", expectedState, state) + } +} + func (s *storeMock) assertLoopInStored() { s.t.Helper()