@@ -574,3 +574,205 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
574574 cost .Server = btcutil .Amount (htlcTx .TxOut [0 ].Value ) - amtPaid
575575 require .Equal (t , cost , finalState .Cost )
576576}
577+
578+ // TestAbandonPublishedHtlcState advances a loop-in swap to StateHtlcPublished,
579+ // then abandons it and ensures that executing the same swap would not progress.
580+ func TestAbandonPublishedHtlcState (t * testing.T ) {
581+ defer test .Guard (t )()
582+
583+ ctx := newLoopInTestContext (t )
584+
585+ height := int32 (600 )
586+
587+ cfg , err , inSwap := startNewLoopIn (t , ctx , height )
588+ require .NoError (t , err )
589+
590+ advanceToPublishedHtlc (t , ctx )
591+
592+ // The client requests to abandon the published htlc state.
593+ inSwap .abandonChan <- struct {}{}
594+
595+ // Ensure that the swap is now in the StateFailAbandoned state.
596+ ctx .assertState (loopdb .StateFailAbandoned )
597+
598+ // Ensure that the swap is also in the StateFailAbandoned state in the
599+ // database.
600+ ctx .store .assertLoopInState (loopdb .StateFailAbandoned )
601+
602+ // Ensure that the swap was abandoned and the execution stopped.
603+ err = <- ctx .errChan
604+ require .Error (t , err )
605+ require .Contains (t , err .Error (), "swap hash abandoned by client" )
606+
607+ // We re-instantiate the swap and ensure that it does not progress.
608+ pendSwap := & loopdb.LoopIn {
609+ Contract : & inSwap .LoopInContract ,
610+ Loop : loopdb.Loop {
611+ Events : []* loopdb.LoopEvent {
612+ {
613+ SwapStateData : loopdb.SwapStateData {
614+ State : inSwap .state ,
615+ },
616+ },
617+ },
618+ Hash : testPreimage .Hash (),
619+ },
620+ }
621+ resumedSwap , err := resumeLoopInSwap (
622+ context .Background (), cfg , pendSwap ,
623+ )
624+ require .NoError (t , err )
625+
626+ // Execute the abandoned swap.
627+ go func () {
628+ err := resumedSwap .execute (
629+ context .Background (), ctx .cfg , height ,
630+ )
631+ if err != nil {
632+ log .Error (err )
633+ }
634+ ctx .errChan <- err
635+ }()
636+
637+ // Ensure that the swap is still in the StateFailAbandoned state.
638+ swapInfo := <- ctx .statusChan
639+ require .Equal (t , loopdb .StateFailAbandoned , swapInfo .State )
640+
641+ // Ensure that the execution flagged the abandoned swap as finalized.
642+ err = <- ctx .errChan
643+ require .Error (t , err )
644+ require .Equal (t , ErrSwapFinalized , err )
645+ }
646+
647+ // TestAbandonSettledInvoiceState advances a loop-in swap to
648+ // StateInvoiceSettled, then abandons it and ensures that executing the same
649+ // swap would not progress.
650+ func TestAbandonSettledInvoiceState (t * testing.T ) {
651+ defer test .Guard (t )()
652+
653+ ctx := newLoopInTestContext (t )
654+
655+ height := int32 (600 )
656+
657+ cfg , err , inSwap := startNewLoopIn (t , ctx , height )
658+ require .NoError (t , err )
659+
660+ advanceToPublishedHtlc (t , ctx )
661+
662+ // Client starts listening for swap invoice updates.
663+ ctx .assertSubscribeInvoice (ctx .server .swapHash )
664+
665+ // Server has already paid invoice before spending the htlc. Signal
666+ // settled.
667+ ctx .updateInvoiceState (49000 , invpkg .ContractSettled )
668+
669+ // Swap is expected to move to the state InvoiceSettled
670+ ctx .assertState (loopdb .StateInvoiceSettled )
671+ ctx .store .assertLoopInState (loopdb .StateInvoiceSettled )
672+
673+ // The client requests to abandon the published htlc state.
674+ inSwap .abandonChan <- struct {}{}
675+
676+ // Ensure that the swap is now in the StateFailAbandoned state.
677+ ctx .assertState (loopdb .StateFailAbandoned )
678+
679+ // Ensure that the swap is also in the StateFailAbandoned state in the
680+ // database.
681+ ctx .store .assertLoopInState (loopdb .StateFailAbandoned )
682+
683+ // Ensure that the swap was abandoned and the execution stopped.
684+ err = <- ctx .errChan
685+ require .Error (t , err )
686+ require .Contains (t , err .Error (), "swap hash abandoned by client" )
687+
688+ // We re-instantiate the swap and ensure that it does not progress.
689+ pendSwap := & loopdb.LoopIn {
690+ Contract : & inSwap .LoopInContract ,
691+ Loop : loopdb.Loop {
692+ Events : []* loopdb.LoopEvent {
693+ {
694+ SwapStateData : loopdb.SwapStateData {
695+ State : inSwap .state ,
696+ },
697+ },
698+ },
699+ Hash : testPreimage .Hash (),
700+ },
701+ }
702+ resumedSwap , err := resumeLoopInSwap (context .Background (), cfg , pendSwap )
703+ require .NoError (t , err )
704+
705+ // Execute the abandoned swap.
706+ go func () {
707+ err := resumedSwap .execute (
708+ context .Background (), ctx .cfg , height ,
709+ )
710+ if err != nil {
711+ log .Error (err )
712+ }
713+ ctx .errChan <- err
714+ }()
715+
716+ // Ensure that the swap is still in the StateFailAbandoned state.
717+ swapInfo := <- ctx .statusChan
718+ require .Equal (t , loopdb .StateFailAbandoned , swapInfo .State )
719+
720+ // Ensure that the execution flagged the abandoned swap as finalized.
721+ err = <- ctx .errChan
722+ require .Error (t , err )
723+ require .Equal (t , ErrSwapFinalized , err )
724+ }
725+
726+ func advanceToPublishedHtlc (t * testing.T , ctx * loopInTestContext ) SwapInfo {
727+ swapInfo := <- ctx .statusChan
728+ require .Equal (t , loopdb .StateInitiated , swapInfo .State )
729+
730+ ctx .assertState (loopdb .StateHtlcPublished )
731+ ctx .store .assertLoopInState (loopdb .StateHtlcPublished )
732+
733+ // Expect htlc to be published.
734+ htlcTx := <- ctx .lnd .SendOutputsChannel
735+
736+ // Expect the same state to be written again with the htlc tx hash
737+ // and on chain fee.
738+ ctx .store .assertLoopInState (loopdb .StateHtlcPublished )
739+
740+ // Expect register for htlc conf (only one, since the htlc is p2tr).
741+ <- ctx .lnd .RegisterConfChannel
742+
743+ // Confirm htlc.
744+ ctx .lnd .ConfChannel <- & chainntnfs.TxConfirmation {
745+ Tx : & htlcTx ,
746+ }
747+
748+ // Client starts listening for spend of htlc.
749+ <- ctx .lnd .RegisterSpendChannel
750+ return swapInfo
751+ }
752+
753+ func startNewLoopIn (t * testing.T , ctx * loopInTestContext , height int32 ) (
754+ * swapConfig , error , * loopInSwap ) {
755+
756+ cfg := newSwapConfig (& ctx .lnd .LndServices , ctx .store , ctx .server )
757+
758+ req := & testLoopInRequest
759+
760+ initResult , err := newLoopInSwap (
761+ context .Background (), cfg ,
762+ height , req ,
763+ )
764+ require .NoError (t , err )
765+
766+ inSwap := initResult .swap
767+
768+ ctx .store .assertLoopInStored ()
769+
770+ go func () {
771+ err := inSwap .execute (context .Background (), ctx .cfg , height )
772+ if err != nil {
773+ log .Error (err )
774+ }
775+ ctx .errChan <- err
776+ }()
777+ return cfg , err , inSwap
778+ }
0 commit comments