diff --git a/staticaddr/loopin/actions.go b/staticaddr/loopin/actions.go index 42ac7f1e..a30a60c3 100644 --- a/staticaddr/loopin/actions.go +++ b/staticaddr/loopin/actions.go @@ -810,10 +810,10 @@ func (f *FSM) SweepHtlcTimeoutAction(ctx context.Context, select { // The context is cancelled when the server is shutting - // down. In that case we give up broadcasting attempts - // and return an error. + // down. Keep the current state so recovery resumes + // broadcasting attempts after restart. case <-ctx.Done(): - return f.HandleError(ctx.Err()) + return fsm.NoOp case <-time.After(htlcTimeoutSweepRetryDelay): } @@ -847,6 +847,10 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, ) if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to register to the htlc timeout "+ "sweep tx: %w", err) @@ -856,6 +860,10 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, for { select { case err := <-errChan: + if ctx.Err() != nil { + return fsm.NoOp + } + return f.HandleError(err) case conf := <-htlcTimeoutTxidChan: @@ -879,7 +887,7 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, return OnHtlcTimeoutSwept case <-ctx.Done(): - return f.HandleError(ctx.Err()) + return fsm.NoOp } } } diff --git a/staticaddr/loopin/actions_test.go b/staticaddr/loopin/actions_test.go index 866c5a5d..6fb61459 100644 --- a/staticaddr/loopin/actions_test.go +++ b/staticaddr/loopin/actions_test.go @@ -207,6 +207,75 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) { } } +// TestSweepHtlcTimeoutActionNoOpOnShutdown ensures that a shutdown during +// timeout sweep publication keeps the FSM in the same state so it can resume +// after restart. +func TestSweepHtlcTimeoutActionNoOpOnShutdown(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + mockLnd := test.NewMockLnd() + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: mockLnd.Client, + WalletKit: mockLnd.WalletKit, + }, + loopIn: &StaticAddressLoopIn{}, + } + + event := f.SweepHtlcTimeoutAction(ctx, nil) + require.Equal(t, fsm.NoOp, event) + require.Nil(t, f.LastActionError) +} + +// TestMonitorHtlcTimeoutSweepActionNoOpOnShutdown ensures that a shutdown +// while waiting for the timeout sweep confirmation keeps the FSM resumable. +func TestMonitorHtlcTimeoutSweepActionNoOpOnShutdown(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + mockLnd := test.NewMockLnd() + sweepAddr, err := mockLnd.WalletKit.NextAddr(ctx, "", 0, false) + require.NoError(t, err) + + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + ChainNotifier: mockLnd.ChainNotifier, + }, + loopIn: &StaticAddressLoopIn{ + HtlcTimeoutSweepAddress: sweepAddr, + InitiationHeight: uint32(mockLnd.Height), + }, + } + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorHtlcTimeoutSweepAction(ctx, nil) + }() + + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("timeout sweep conf registration not received: %v", + ctx.Err()) + } + + cancel() + + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + require.Nil(t, f.LastActionError) + + case <-time.After(5 * time.Second): + t.Fatal("timeout sweep monitor did not return") + } +} + // TestInitHtlcActionPreservesRouteHints asserts that static-address loop-in // propagates explicit route hints into the encoded swap invoice sent to the // server.