@@ -9,9 +9,13 @@ import (
99 "github.com/btcsuite/btcd/chaincfg/chainhash"
1010 "github.com/btcsuite/btcd/txscript"
1111 "github.com/btcsuite/btcd/wire"
12+ "github.com/lightninglabs/loop/fsm"
13+ "github.com/lightninglabs/loop/staticaddr/address"
1214 "github.com/lightninglabs/loop/staticaddr/deposit"
15+ "github.com/lightninglabs/loop/staticaddr/script"
1316 "github.com/lightninglabs/loop/swapserverrpc"
1417 "github.com/lightninglabs/loop/test"
18+ "github.com/lightningnetwork/lnd/build"
1519 "github.com/lightningnetwork/lnd/funding"
1620 "github.com/lightningnetwork/lnd/input"
1721 "github.com/lightningnetwork/lnd/lnrpc"
@@ -20,6 +24,10 @@ import (
2024 "github.com/stretchr/testify/require"
2125)
2226
27+ func init () {
28+ UseLogger (build .NewSubLogger ("WDRW" , nil ))
29+ }
30+
2331// TestNewManagerHeightValidation ensures the constructor rejects zero heights.
2432func TestNewManagerHeightValidation (t * testing.T ) {
2533 t .Parallel ()
@@ -606,3 +614,190 @@ func TestCalculateWithdrawalTxValues(t *testing.T) {
606614 })
607615 }
608616}
617+
618+ // recoveryDepositManager is a test stub that tracks recovery interactions for
619+ // deposits in the WITHDRAWING state.
620+ type recoveryDepositManager struct {
621+ withdrawingDeposits []* deposit.Deposit
622+ transitioned [][]wire.OutPoint
623+ updated []wire.OutPoint
624+ }
625+
626+ // GetActiveDepositsInState returns the preset withdrawing deposits for the
627+ // recovery test.
628+ func (m * recoveryDepositManager ) GetActiveDepositsInState (
629+ _ fsm.StateType ) (
630+ []* deposit.Deposit , error ) {
631+
632+ return m .withdrawingDeposits , nil
633+ }
634+
635+ // AllOutpointsActiveDeposits reports no active deposit set lookup in this
636+ // test stub.
637+ func (m * recoveryDepositManager ) AllOutpointsActiveDeposits (
638+ _ []wire.OutPoint , _ fsm.StateType ) ([]* deposit.Deposit , bool ) {
639+
640+ return nil , false
641+ }
642+
643+ // TransitionDeposits records the outpoints transitioned by recovery.
644+ func (m * recoveryDepositManager ) TransitionDeposits (_ context.Context ,
645+ deposits []* deposit.Deposit , _ fsm.EventType , _ fsm.StateType ) error {
646+
647+ outpoints := make ([]wire.OutPoint , len (deposits ))
648+ for i , d := range deposits {
649+ outpoints [i ] = d .OutPoint
650+ }
651+
652+ m .transitioned = append (m .transitioned , outpoints )
653+
654+ return nil
655+ }
656+
657+ // UpdateDeposit records which deposits were updated during recovery.
658+ func (m * recoveryDepositManager ) UpdateDeposit (_ context.Context ,
659+ d * deposit.Deposit ) error {
660+
661+ m .updated = append (m .updated , d .OutPoint )
662+
663+ return nil
664+ }
665+
666+ // recoveryAddressManager is a test stub that serves static address parameters
667+ // needed by withdrawal recovery.
668+ type recoveryAddressManager struct {
669+ params * address.Parameters
670+ }
671+
672+ // GetStaticAddressParameters returns the preset static address parameters for
673+ // the recovery test.
674+ func (m * recoveryAddressManager ) GetStaticAddressParameters (
675+ _ context.Context ) (* address.Parameters , error ) {
676+
677+ return m .params , nil
678+ }
679+
680+ // GetStaticAddress returns no static address in this test stub.
681+ func (m * recoveryAddressManager ) GetStaticAddress (
682+ _ context.Context ) (* script.StaticAddress , error ) {
683+
684+ return nil , nil
685+ }
686+
687+ // TestRecoverWithdrawalsIncludesMissingFinalizedTxDeposits verifies regression
688+ // coverage for restart recovery where some deposits are in WITHDRAWING but
689+ // missing FinalizedWithdrawalTx pointers.
690+ //
691+ // Without the fix this test still builds, but fails at runtime because the
692+ // legacy recovery code silently skips those deposits and only reinstates the
693+ // subset with non-nil FinalizedWithdrawalTx.
694+ func TestRecoverWithdrawalsIncludesMissingFinalizedTxDeposits (t * testing.T ) {
695+ t .Parallel ()
696+
697+ tx := wire .NewMsgTx (2 )
698+ tx .AddTxIn (& wire.TxIn {
699+ PreviousOutPoint : wire.OutPoint {
700+ Hash : chainhash.Hash {9 },
701+ Index : 0 ,
702+ },
703+ })
704+ tx .AddTxOut (& wire.TxOut {
705+ Value : 1000 ,
706+ PkScript : []byte {txscript .OP_1 },
707+ })
708+
709+ known1 := & deposit.Deposit {
710+ OutPoint : wire.OutPoint {
711+ Hash : chainhash.Hash {1 },
712+ Index : 0 ,
713+ },
714+ ConfirmationHeight : 100 ,
715+ FinalizedWithdrawalTx : tx ,
716+ }
717+ known2 := & deposit.Deposit {
718+ OutPoint : wire.OutPoint {
719+ Hash : chainhash.Hash {2 },
720+ Index : 0 ,
721+ },
722+ ConfirmationHeight : 100 ,
723+ FinalizedWithdrawalTx : tx ,
724+ }
725+ missing1 := & deposit.Deposit {
726+ OutPoint : wire.OutPoint {
727+ Hash : chainhash.Hash {3 },
728+ Index : 0 ,
729+ },
730+ ConfirmationHeight : 100 ,
731+ }
732+ missing2 := & deposit.Deposit {
733+ OutPoint : wire.OutPoint {
734+ Hash : chainhash.Hash {4 },
735+ Index : 0 ,
736+ },
737+ ConfirmationHeight : 100 ,
738+ }
739+
740+ depositMgr := & recoveryDepositManager {
741+ withdrawingDeposits : []* deposit.Deposit {
742+ known1 , known2 , missing1 , missing2 ,
743+ },
744+ }
745+ addrMgr := & recoveryAddressManager {
746+ params : & address.Parameters {
747+ PkScript : []byte {txscript .OP_1 },
748+ },
749+ }
750+
751+ lnd := test .NewMockLnd ()
752+ go func () {
753+ <- lnd .TxPublishChannel
754+ }()
755+ go func () {
756+ <- lnd .RegisterSpendChannel
757+ }()
758+
759+ mgr , err := NewManager (& ManagerConfig {
760+ DepositManager : depositMgr ,
761+ WalletKit : lnd .WalletKit ,
762+ ChainNotifier : lnd .ChainNotifier ,
763+ AddressManager : addrMgr ,
764+ }, 101 )
765+ require .NoError (t , err )
766+
767+ ctx , cancel := context .WithCancel (context .Background ())
768+ defer cancel ()
769+
770+ err = mgr .recoverWithdrawals (ctx )
771+ require .NoError (t , err )
772+
773+ // Assert we re-instated one withdrawal cluster containing all four
774+ // deposits. The old buggy behavior re-instated only the two deposits
775+ // that already had finalized tx pointers.
776+ require .Len (t , depositMgr .transitioned , 1 )
777+ require .Len (t , depositMgr .transitioned [0 ], 4 )
778+
779+ transitioned := make (map [wire.OutPoint ]struct {})
780+ for _ , op := range depositMgr .transitioned [0 ] {
781+ transitioned [op ] = struct {}{}
782+ }
783+ _ , ok := transitioned [missing1 .OutPoint ]
784+ require .True (t , ok )
785+ _ , ok = transitioned [missing2 .OutPoint ]
786+ require .True (t , ok )
787+
788+ // Missing pointers should be recovered and persisted.
789+ updated := make (map [wire.OutPoint ]struct {})
790+ for _ , op := range depositMgr .updated {
791+ updated [op ] = struct {}{}
792+ }
793+ _ , ok = updated [missing1 .OutPoint ]
794+ require .True (t , ok )
795+ _ , ok = updated [missing2 .OutPoint ]
796+ require .True (t , ok )
797+ require .NotNil (t , missing1 .FinalizedWithdrawalTx )
798+ require .NotNil (t , missing2 .FinalizedWithdrawalTx )
799+
800+ // Shut down notifier goroutines started by recovery.
801+ cancel ()
802+ lnd .WaitForFinished ()
803+ }
0 commit comments