diff --git a/client_test.go b/client_test.go index e9b8641..c6c942c 100644 --- a/client_test.go +++ b/client_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/test" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" @@ -146,8 +147,6 @@ func TestLoopOutFailWrongAmount(t *testing.T) { // TestLoopOutResume tests that swaps in various states are properly resumed // after a restart. func TestLoopOutResume(t *testing.T) { - defer test.Guard(t)() - defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations storedVersion := []loopdb.ProtocolVersion{ @@ -279,7 +278,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed, preimageRevealed, int32(confs), ) - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( hash, &pendingSwap.Contract.SwapContract, &chaincfg.TestNet3Params, ) @@ -304,7 +303,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed, func(r error) {}, func(r error) {}, preimageRevealed, - confIntent, GetHtlcScriptVersion(protocolVersion), + confIntent, utils.GetHtlcScriptVersion(protocolVersion), ) } @@ -317,15 +316,28 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, 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) + // We need to notify the height, as the loopout is going to attempt a + // sweep when a new block is received. + err := ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1) + require.NoError(ctx.Context.T, err) + // Publish tick. ctx.expiryChan <- testTime + // One spend notifier is registered by batch to watch primary sweep. + ctx.AssertRegisterSpendNtfn(confIntent.PkScript) + + ctx.AssertEpochListeners(2) + + // Mock the blockheight again as that's when the batch will broadcast + // the tx. + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1) + require.NoError(ctx.Context.T, err) + // Expect a signing request in the non taproot case. if scriptVersion != swap.HtlcV3 { <-ctx.Context.Lnd.SignOutputRawChannel @@ -341,13 +353,6 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, // our MuSig2 signing attempts. if scriptVersion == swap.HtlcV3 { ctx.assertPreimagePush(ctx.store.LoopOutSwaps[hash].Preimage) - - // Try MuSig2 signing first and fail it so that we go for a - // normal sweep. - for i := 0; i < maxMusigSweepRetries; i++ { - ctx.expiryChan <- testTime - ctx.assertPreimagePush(ctx.store.LoopOutSwaps[hash].Preimage) - } <-ctx.Context.Lnd.SignOutputRawChannel } @@ -388,6 +393,8 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, ctx.NotifySpend(sweepTx, 0) + ctx.AssertRegisterConf(true, 3) + ctx.assertStatus(loopdb.StateSuccess) ctx.assertStoreFinished(loopdb.StateSuccess) diff --git a/loopin_test.go b/loopin_test.go index 7c27abe..768869f 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/test" + "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/routing/route" @@ -449,7 +450,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, pendSwap.Loop.Events[0].Cost = cost } - htlc, err := GetHtlc( + htlc, err := utils.GetHtlc( testPreimage.Hash(), &contract.SwapContract, cfg.lnd.ChainParams, ) diff --git a/loopout_test.go b/loopout_test.go index e05919e..dfbeb62 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -14,6 +14,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -293,13 +294,33 @@ func testCustomSweepConfTarget(t *testing.T) { return expiryChan } - errChan := make(chan error) + errChan := make(chan error, 2) + + batcherStore := sweepbatcher.NewStoreMock() + + batcher := sweepbatcher.NewBatcher( + lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, + mockMuSig2SignSweep, mockVerifySchnorrSigSuccess, + lnd.ChainParams, batcherStore, cfg.store, + ) + + tctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - err := swap.execute(context.Background(), &executeConfig{ + err := batcher.Run(tctx) + if err != nil { + errChan <- err + } + }() + + go func() { + err := swap.execute(tctx, &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail, }, ctx.Lnd.Height) @@ -335,16 +356,21 @@ func testCustomSweepConfTarget(t *testing.T) { 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) - // Assert that we made a query to track our payment, as required for // preimage push tracking. trackPayment := ctx.AssertTrackPayment() expiryChan <- time.Now() + // 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) + + ctx.AssertEpochListeners(1) + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1) + require.NoError(t, err) + // Expect a signing request for the HTLC success transaction. if !IsTaprootSwap(&swap.SwapContract) { <-ctx.Lnd.SignOutputRawChannel @@ -409,7 +435,7 @@ func testCustomSweepConfTarget(t *testing.T) { // The sweep should have a fee that corresponds to the custom // confirmation target. - _ = assertSweepTx(testReq.SweepConfTarget) + sweepTx := assertSweepTx(testReq.SweepConfTarget) // Once we have published an on chain sweep, we expect a preimage to // have been pushed to our server. @@ -426,23 +452,13 @@ func testCustomSweepConfTarget(t *testing.T) { State: lnrpc.Payment_SUCCEEDED, } - // We'll then notify the height at which we begin using the default - // confirmation target. - defaultConfTargetHeight := ctx.Lnd.Height + - testLoopOutMinOnChainCltvDelta - DefaultSweepConfTargetDelta - blockEpochChan <- defaultConfTargetHeight - expiryChan <- time.Now() - - // Expect another signing request. - <-ctx.Lnd.SignOutputRawChannel - - // 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. + // Notify the batch for the spend. ctx.NotifySpend(sweepTx, 0) + // After receiving the notification the batch will start monitoring the + // confirmations. + ctx.AssertRegisterConf(true, 3) + cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess) status = <-statusChan require.Equal(t, loopdb.StateSuccess, status.State) @@ -511,13 +527,33 @@ func testPreimagePush(t *testing.T) { return expiryChan } - errChan := make(chan error) + errChan := make(chan error, 2) + + batcherStore := sweepbatcher.NewStoreMock() + + batcher := sweepbatcher.NewBatcher( + lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, + mockMuSig2SignSweep, mockVerifySchnorrSigSuccess, + lnd.ChainParams, batcherStore, cfg.store, + ) + + tctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + err := batcher.Run(tctx) + if err != nil { + errChan <- err + } + }() + go func() { err := swap.execute(context.Background(), &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail, }, ctx.Lnd.Height) @@ -553,10 +589,6 @@ func testPreimagePush(t *testing.T) { 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) - // Assert that we made a query to track our payment, as required for // preimage push tracking. trackPayment := ctx.AssertTrackPayment() @@ -567,6 +599,15 @@ func testPreimagePush(t *testing.T) { // preimage is not revealed, we also do not expect a preimage push. expiryChan <- testTime + // 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) + + ctx.AssertEpochListeners(1) + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1) + require.NoError(t, err) + // When using taproot htlcs the flow is different as we do reveal the // preimage before sweeping in order for the server to trust us with // our MuSig2 signing attempts. @@ -582,15 +623,6 @@ func testPreimagePush(t *testing.T) { preimage := <-server.preimagePush require.Equal(t, swap.Preimage, preimage) - // Try MuSig2 signing first and fail it so that we go for a - // normal sweep. - for i := 0; i < maxMusigSweepRetries; i++ { - expiryChan <- time.Now() - - preimage := <-server.preimagePush - require.Equal(t, swap.Preimage, preimage) - } - <-ctx.Lnd.SignOutputRawChannel // We expect the sweep tx to have been published. @@ -611,6 +643,10 @@ func testPreimagePush(t *testing.T) { // Now when we report a new block and tick our expiry fee timer, and // fees are acceptably low so we expect our sweep to be published. blockEpochChan <- ctx.Lnd.Height + 2 + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + expiryChan <- testTime if IsTaprootSwap(&swap.SwapContract) { @@ -648,6 +684,10 @@ func testPreimagePush(t *testing.T) { // chain yet so we can test our preimage push retry logic. Instead, we // tick the expiry chan again to prompt another sweep. expiryChan <- testTime + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + if IsTaprootSwap(&swap.SwapContract) { preimage := <-server.preimagePush require.Equal(t, swap.Preimage, preimage) @@ -678,6 +718,10 @@ func testPreimagePush(t *testing.T) { // push. The test's mocked preimage channel is un-buffered, so our test // would hang if we pushed the preimage here. expiryChan <- testTime + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + <-ctx.Lnd.SignOutputRawChannel sweepTx := ctx.ReceiveTx() @@ -685,6 +729,10 @@ func testPreimagePush(t *testing.T) { // spend our sweepTx and assert that the swap succeeds. ctx.NotifySpend(sweepTx, 0) + // After receiving the spend ntfn the batch will start monitoring for + // confs. + ctx.AssertRegisterConf(true, 3) + cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess) status := <-statusChan require.Equal( @@ -892,8 +940,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { return expiryChan } - errChan := make(chan error) - // Mock a successful signature verify to make sure we don't fail // creating the MuSig2 sweep. mockVerifySchnorrSigSuccess := func(pubKey *btcec.PublicKey, hash, @@ -902,12 +948,33 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { return nil } + errChan := make(chan error, 2) + + batcherStore := sweepbatcher.NewStoreMock() + + batcher := sweepbatcher.NewBatcher( + lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, + mockMuSig2SignSweep, mockVerifySchnorrSigSuccess, + lnd.ChainParams, batcherStore, cfg.store, + ) + + tctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + err := batcher.Run(tctx) + if err != nil { + errChan <- err + } + }() + go func() { err := swap.execute(context.Background(), &executeConfig{ statusChan: statusChan, blockEpochChan: blockEpochChan, timerFactory: timerFactory, sweeper: sweeper, + batcher: batcher, cancelSwap: server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigSuccess, }, ctx.Lnd.Height) @@ -943,10 +1010,6 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { 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) - // Assert that we made a query to track our payment, as required for // preimage push tracking. trackPayment := ctx.AssertTrackPayment() @@ -957,6 +1020,15 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { // preimage is not revealed, we also do not expect a preimage push. expiryChan <- testTime + // 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) + + ctx.AssertEpochListeners(1) + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1) + require.NoError(t, err) + // When using taproot htlcs the flow is different as we do reveal the // preimage before sweeping in order for the server to trust us with // our MuSig2 signing attempts. @@ -988,6 +1060,10 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { // Now when we report a new block and tick our expiry fee timer, and // fees are acceptably low so we expect our sweep to be published. blockEpochChan <- ctx.Lnd.Height + 2 + + err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 2) + require.NoError(t, err) + expiryChan <- testTime preimage = <-server.preimagePush @@ -1010,6 +1086,10 @@ func TestLoopOutMuSig2Sweep(t *testing.T) { // spend our sweepTx and assert that the swap succeeds. ctx.NotifySpend(sweepTx, 0) + // After receiving the spend ntfn the batch will start monitoring for + // confs. + ctx.AssertRegisterConf(true, 3) + cfg.store.(*loopdb.StoreMock).AssertLoopOutState(loopdb.StateSuccess) status = <-statusChan require.Equal(t, status.State, loopdb.StateSuccess) diff --git a/testcontext_test.go b/testcontext_test.go index 47b1488..423eb31 100644 --- a/testcontext_test.go +++ b/testcontext_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" + "github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnrpc" @@ -51,6 +52,24 @@ func mockVerifySchnorrSigFail(pubKey *btcec.PublicKey, hash, return fmt.Errorf("invalid sig") } +// mockVerifySchnorrSigSuccess is used to simulate successful taproot keyspend +// signature verification. If passed to the executeConfig we'll test an +// uncooperative server and will fall back to scriptspend sweep. +func mockVerifySchnorrSigSuccess(pubKey *btcec.PublicKey, hash, + sig []byte) error { + + return fmt.Errorf("invalid sig") +} + +func mockMuSig2SignSweep(ctx context.Context, + protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash, + paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte, + prevoutMap map[wire.OutPoint]*wire.TxOut) ( + []byte, []byte, error) { + + return nil, nil, nil +} + func newSwapClient(config *clientConfig) *Client { sweeper := &sweep.Sweeper{ Lnd: config.LndServices, @@ -58,10 +77,20 @@ func newSwapClient(config *clientConfig) *Client { lndServices := config.LndServices + batcherStore := sweepbatcher.NewStoreMock() + + batcher := sweepbatcher.NewBatcher( + config.LndServices.WalletKit, config.LndServices.ChainNotifier, + config.LndServices.Signer, mockMuSig2SignSweep, + mockVerifySchnorrSigSuccess, config.LndServices.ChainParams, + batcherStore, config.Store, + ) + executor := newExecutor(&executorConfig{ lnd: lndServices, store: config.Store, sweeper: sweeper, + batcher: batcher, createExpiryTimer: config.CreateExpiryTimer, cancelSwap: config.Server.CancelLoopOutSwap, verifySchnorrSig: mockVerifySchnorrSigFail,