diff --git a/loopin_test.go b/loopin_test.go index 52dee18..b648d8c 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -574,3 +574,205 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, cost.Server = btcutil.Amount(htlcTx.TxOut[0].Value) - amtPaid require.Equal(t, cost, finalState.Cost) } + +// TestAbandonPublishedHtlcState advances a loop-in swap to StateHtlcPublished, +// then abandons it and ensures that executing the same swap would not progress. +func TestAbandonPublishedHtlcState(t *testing.T) { + defer test.Guard(t)() + + ctx := newLoopInTestContext(t) + + height := int32(600) + + cfg, err, inSwap := startNewLoopIn(t, ctx, height) + require.NoError(t, err) + + advanceToPublishedHtlc(t, ctx) + + // The client requests to abandon the published htlc state. + inSwap.abandonChan <- struct{}{} + + // Ensure that the swap is now in the StateFailAbandoned state. + ctx.assertState(loopdb.StateFailAbandoned) + + // Ensure that the swap is also in the StateFailAbandoned state in the + // database. + ctx.store.assertLoopInState(loopdb.StateFailAbandoned) + + // Ensure that the swap was abandoned and the execution stopped. + err = <-ctx.errChan + require.Error(t, err) + require.Contains(t, err.Error(), "swap hash abandoned by client") + + // We re-instantiate the swap and ensure that it does not progress. + pendSwap := &loopdb.LoopIn{ + Contract: &inSwap.LoopInContract, + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: inSwap.state, + }, + }, + }, + Hash: testPreimage.Hash(), + }, + } + resumedSwap, err := resumeLoopInSwap( + context.Background(), cfg, pendSwap, + ) + require.NoError(t, err) + + // Execute the abandoned swap. + go func() { + err := resumedSwap.execute( + context.Background(), ctx.cfg, height, + ) + if err != nil { + log.Error(err) + } + ctx.errChan <- err + }() + + // Ensure that the swap is still in the StateFailAbandoned state. + swapInfo := <-ctx.statusChan + require.Equal(t, loopdb.StateFailAbandoned, swapInfo.State) + + // Ensure that the execution flagged the abandoned swap as finalized. + err = <-ctx.errChan + require.Error(t, err) + require.Equal(t, ErrSwapFinalized, err) +} + +// TestAbandonSettledInvoiceState advances a loop-in swap to +// StateInvoiceSettled, then abandons it and ensures that executing the same +// swap would not progress. +func TestAbandonSettledInvoiceState(t *testing.T) { + defer test.Guard(t)() + + ctx := newLoopInTestContext(t) + + height := int32(600) + + cfg, err, inSwap := startNewLoopIn(t, ctx, height) + require.NoError(t, err) + + advanceToPublishedHtlc(t, ctx) + + // Client starts listening for swap invoice updates. + ctx.assertSubscribeInvoice(ctx.server.swapHash) + + // Server has already paid invoice before spending the htlc. Signal + // settled. + ctx.updateInvoiceState(49000, invpkg.ContractSettled) + + // Swap is expected to move to the state InvoiceSettled + ctx.assertState(loopdb.StateInvoiceSettled) + ctx.store.assertLoopInState(loopdb.StateInvoiceSettled) + + // The client requests to abandon the published htlc state. + inSwap.abandonChan <- struct{}{} + + // Ensure that the swap is now in the StateFailAbandoned state. + ctx.assertState(loopdb.StateFailAbandoned) + + // Ensure that the swap is also in the StateFailAbandoned state in the + // database. + ctx.store.assertLoopInState(loopdb.StateFailAbandoned) + + // Ensure that the swap was abandoned and the execution stopped. + err = <-ctx.errChan + require.Error(t, err) + require.Contains(t, err.Error(), "swap hash abandoned by client") + + // We re-instantiate the swap and ensure that it does not progress. + pendSwap := &loopdb.LoopIn{ + Contract: &inSwap.LoopInContract, + Loop: loopdb.Loop{ + Events: []*loopdb.LoopEvent{ + { + SwapStateData: loopdb.SwapStateData{ + State: inSwap.state, + }, + }, + }, + Hash: testPreimage.Hash(), + }, + } + resumedSwap, err := resumeLoopInSwap(context.Background(), cfg, pendSwap) + require.NoError(t, err) + + // Execute the abandoned swap. + go func() { + err := resumedSwap.execute( + context.Background(), ctx.cfg, height, + ) + if err != nil { + log.Error(err) + } + ctx.errChan <- err + }() + + // Ensure that the swap is still in the StateFailAbandoned state. + swapInfo := <-ctx.statusChan + require.Equal(t, loopdb.StateFailAbandoned, swapInfo.State) + + // Ensure that the execution flagged the abandoned swap as finalized. + err = <-ctx.errChan + require.Error(t, err) + require.Equal(t, ErrSwapFinalized, err) +} + +func advanceToPublishedHtlc(t *testing.T, ctx *loopInTestContext) SwapInfo { + swapInfo := <-ctx.statusChan + require.Equal(t, loopdb.StateInitiated, swapInfo.State) + + ctx.assertState(loopdb.StateHtlcPublished) + ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + + // Expect htlc to be published. + htlcTx := <-ctx.lnd.SendOutputsChannel + + // Expect the same state to be written again with the htlc tx hash + // and on chain fee. + ctx.store.assertLoopInState(loopdb.StateHtlcPublished) + + // Expect register for htlc conf (only one, since the htlc is p2tr). + <-ctx.lnd.RegisterConfChannel + + // Confirm htlc. + ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{ + Tx: &htlcTx, + } + + // Client starts listening for spend of htlc. + <-ctx.lnd.RegisterSpendChannel + return swapInfo +} + +func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) ( + *swapConfig, error, *loopInSwap) { + + cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server) + + req := &testLoopInRequest + + initResult, err := newLoopInSwap( + context.Background(), cfg, + height, req, + ) + require.NoError(t, err) + + inSwap := initResult.swap + + ctx.store.assertLoopInStored() + + go func() { + err := inSwap.execute(context.Background(), ctx.cfg, height) + if err != nil { + log.Error(err) + } + ctx.errChan <- err + }() + return cfg, err, inSwap +} diff --git a/loopin_testcontext_test.go b/loopin_testcontext_test.go index 7d3ffcd..6bf0ad1 100644 --- a/loopin_testcontext_test.go +++ b/loopin_testcontext_test.go @@ -22,6 +22,7 @@ type loopInTestContext struct { sweeper *sweep.Sweeper cfg *executeConfig statusChan chan SwapInfo + errChan chan error blockEpochChan chan interface{} swapInvoiceSubscription *test.SingleInvoiceSubscription @@ -35,6 +36,7 @@ func newLoopInTestContext(t *testing.T) *loopInTestContext { blockEpochChan := make(chan interface{}) statusChan := make(chan SwapInfo) + errChan := make(chan error) expiryChan := make(chan time.Time) timerFactory := func(expiry time.Duration) <-chan time.Time { @@ -57,6 +59,7 @@ func newLoopInTestContext(t *testing.T) *loopInTestContext { sweeper: &sweeper, cfg: &cfg, statusChan: statusChan, + errChan: errChan, blockEpochChan: blockEpochChan, } } diff --git a/testcontext_test.go b/testcontext_test.go index 2e4b996..e465037 100644 --- a/testcontext_test.go +++ b/testcontext_test.go @@ -125,10 +125,7 @@ func createClientTestContext(t *testing.T, ctx.stop = stop go func() { - err := swapClient.Run( - runCtx, - statusChan, - ) + err := swapClient.Run(runCtx, statusChan) log.Errorf("client run: %v", err) ctx.runErr <- err }()