package loop import ( "bytes" "context" "crypto/sha256" "errors" "testing" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" ) var ( testAddr, _ = btcutil.NewAddressScriptHash( []byte{123}, &chaincfg.TestNet3Params, ) testRequest = &OutRequest{ Amount: btcutil.Amount(50000), DestAddr: testAddr, MaxMinerFee: 50000, HtlcConfirmations: defaultConfirmations, SweepConfTarget: 2, MaxSwapFee: 1050, MaxPrepayAmount: 100, MaxPrepayRoutingFee: 75000, MaxSwapRoutingFee: 70000, Initiator: "test", } swapInvoiceDesc = "swap" prepayInvoiceDesc = "prepay" defaultConfirmations = int32(loopdb.DefaultLoopOutHtlcConfirmations) ) // TestSuccess tests the loop out happy flow, using a custom htlc confirmation // target. func TestSuccess(t *testing.T) { defer test.Guard(t)() ctx := createClientTestContext(t, nil) req := *testRequest req.HtlcConfirmations = 2 // Initiate loop out. info, err := ctx.swapClient.LoopOut(context.Background(), &req) if err != nil { t.Fatal(err) } ctx.assertStored() ctx.assertStatus(loopdb.StateInitiated) signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) // Expect client to register for conf. confIntent := ctx.AssertRegisterConf(false, req.HtlcConfirmations) testSuccess(ctx, testRequest.Amount, info.SwapHash, signalPrepaymentResult, signalSwapPaymentResult, false, confIntent, swap.HtlcV2, ) } // TestFailOffchain tests the handling of swap for which the server failed the // payments. func TestFailOffchain(t *testing.T) { defer test.Guard(t)() ctx := createClientTestContext(t, nil) _, err := ctx.swapClient.LoopOut(context.Background(), testRequest) if err != nil { t.Fatal(err) } ctx.assertStored() ctx.assertStatus(loopdb.StateInitiated) signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) ctx.AssertRegisterConf(false, defaultConfirmations) signalSwapPaymentResult( errors.New(lndclient.PaymentResultUnknownPaymentHash), ) signalPrepaymentResult( errors.New(lndclient.PaymentResultUnknownPaymentHash), ) ctx.assertStatus(loopdb.StateFailOffchainPayments) ctx.assertStoreFinished(loopdb.StateFailOffchainPayments) ctx.finish() } // TestWrongAmount asserts that the client checks the server invoice amounts. func TestFailWrongAmount(t *testing.T) { defer test.Guard(t)() test := func(t *testing.T, modifier func(*serverMock), expectedErr error) { ctx := createClientTestContext(t, nil) // Modify mock for this subtest. modifier(ctx.serverMock) _, err := ctx.swapClient.LoopOut( context.Background(), testRequest, ) if err != expectedErr { t.Fatalf("Expected %v, but got %v", expectedErr, err) } ctx.finish() } t.Run("swap fee too high", func(t *testing.T) { test(t, func(m *serverMock) { m.swapInvoiceAmt += 10 }, ErrSwapFeeTooHigh) }) t.Run("prepay amount too high", func(t *testing.T) { test(t, func(m *serverMock) { // Keep total swap fee unchanged, but increase prepaid // portion. m.swapInvoiceAmt -= 10 m.prepayInvoiceAmt += 10 }, ErrPrepayAmountTooHigh) }) } // TestResume tests that swaps in various states are properly resumed after a // restart. func TestResume(t *testing.T) { defer test.Guard(t)() defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations storedVersion := []loopdb.ProtocolVersion{ loopdb.ProtocolVersionUnrecorded, loopdb.ProtocolVersionHtlcV2, } for _, version := range storedVersion { version := version t.Run(version.String(), func(t *testing.T) { t.Run("not expired", func(t *testing.T) { testResume( t, defaultConfs, false, false, true, version, ) }) t.Run("not expired, custom confirmations", func(t *testing.T) { testResume( t, 3, false, false, true, version, ) }) t.Run("expired not revealed", func(t *testing.T) { testResume( t, defaultConfs, true, false, false, version, ) }) t.Run("expired revealed", func(t *testing.T) { testResume( t, defaultConfs, true, true, true, version, ) }) }) } } func testResume(t *testing.T, confs uint32, expired, preimageRevealed, expectSuccess bool, protocolVersion loopdb.ProtocolVersion) { defer test.Guard(t)() preimage := testPreimage hash := sha256.Sum256(preimage[:]) dest := test.GetDestAddr(t, 0) amt := btcutil.Amount(50000) swapPayReq, err := getInvoice(hash, amt, swapInvoiceDesc) if err != nil { t.Fatal(err) } prePayReq, err := getInvoice(hash, 100, prepayInvoiceDesc) if err != nil { t.Fatal(err) } _, senderPubKey := test.CreateKey(1) var senderKey [33]byte copy(senderKey[:], senderPubKey.SerializeCompressed()) _, receiverPubKey := test.CreateKey(2) var receiverKey [33]byte copy(receiverKey[:], receiverPubKey.SerializeCompressed()) update := loopdb.LoopEvent{ SwapStateData: loopdb.SwapStateData{ State: loopdb.StateInitiated, }, } if preimageRevealed { update.State = loopdb.StatePreimageRevealed update.HtlcTxHash = &chainhash.Hash{1, 2, 6} } // Create a pending swap with our custom number of confirmations. pendingSwap := &loopdb.LoopOut{ Contract: &loopdb.LoopOutContract{ DestAddr: dest, SwapInvoice: swapPayReq, SweepConfTarget: 2, HtlcConfirmations: confs, MaxSwapRoutingFee: 70000, PrepayInvoice: prePayReq, SwapContract: loopdb.SwapContract{ Preimage: preimage, AmountRequested: amt, CltvExpiry: 744, ReceiverKey: receiverKey, SenderKey: senderKey, MaxSwapFee: 60000, MaxMinerFee: 50000, ProtocolVersion: protocolVersion, }, }, Loop: loopdb.Loop{ Events: []*loopdb.LoopEvent{&update}, Hash: hash, }, } if expired { // Set cltv expiry so that it has already expired at the test // block height. pendingSwap.Contract.CltvExpiry = 610 } ctx := createClientTestContext(t, []*loopdb.LoopOut{pendingSwap}) if preimageRevealed { ctx.assertStatus(loopdb.StatePreimageRevealed) } else { ctx.assertStatus(loopdb.StateInitiated) } signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) // Expect client to register for our expected number of confirmations. confIntent := ctx.AssertRegisterConf(preimageRevealed, int32(confs)) // Assert that the loopout htlc equals to the expected one. scriptVersion := GetHtlcScriptVersion(protocolVersion) htlc, err := swap.NewHtlc( scriptVersion, pendingSwap.Contract.CltvExpiry, senderKey, receiverKey, hash, swap.HtlcP2WSH, &chaincfg.TestNet3Params, ) require.NoError(t, err) require.Equal(t, htlc.PkScript, confIntent.PkScript) signalSwapPaymentResult(nil) signalPrepaymentResult(nil) if !expectSuccess { ctx.assertStatus(loopdb.StateFailTimeout) ctx.assertStoreFinished(loopdb.StateFailTimeout) ctx.finish() return } // Because there is no reliable payment yet, an invoice is assumed to be // paid after resume. testSuccess(ctx, amt, hash, func(r error) {}, func(r error) {}, preimageRevealed, confIntent, scriptVersion, ) } func testSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, signalPrepaymentResult, signalSwapPaymentResult func(error), preimageRevealed bool, confIntent *test.ConfRegistration, scriptVersion swap.ScriptVersion) { htlcOutpoint := ctx.publishHtlc(confIntent.PkScript, amt) signalPrepaymentResult(nil) ctx.AssertRegisterSpendNtfn(confIntent.PkScript) // Assert that a call to track payment was sent, and respond with status // in flight so that our swap will push its preimage to the server. ctx.trackPayment(lnrpc.Payment_IN_FLIGHT) // Publish tick. ctx.expiryChan <- testTime // Expect a signing request. <-ctx.Lnd.SignOutputRawChannel if !preimageRevealed { ctx.assertStatus(loopdb.StatePreimageRevealed) ctx.assertStorePreimageReveal() } // Expect client on-chain sweep of HTLC. sweepTx := ctx.ReceiveTx() if !bytes.Equal(sweepTx.TxIn[0].PreviousOutPoint.Hash[:], htlcOutpoint.Hash[:]) { ctx.T.Fatalf("client not sweeping from htlc tx") } preImageIndex := 1 if scriptVersion == swap.HtlcV2 { preImageIndex = 0 } // Check preimage. clientPreImage := sweepTx.TxIn[0].Witness[preImageIndex] clientPreImageHash := sha256.Sum256(clientPreImage) if clientPreImageHash != hash { ctx.T.Fatalf("incorrect preimage") } // Since we successfully published our sweep, we expect the preimage to // have been pushed to our mock server. preimage, err := lntypes.MakePreimage(clientPreImage) require.NoError(ctx.T, err) ctx.assertPreimagePush(preimage) // Simulate server pulling payment. signalSwapPaymentResult(nil) ctx.NotifySpend(sweepTx, 0) ctx.assertStatus(loopdb.StateSuccess) ctx.assertStoreFinished(loopdb.StateSuccess) ctx.finish() }