Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions instantout/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,32 @@
timer := time.NewTimer(time.Second)
for {
select {
case payRes := <-payChan:
case payRes, ok := <-payChan:
if !ok {
return f.handleErrorAndUnlockReservations(
ctx, errors.New("payment status channel closed"),
)
}
Comment on lines +247 to +252
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Failing the swap when payChan is closed might be premature. In many cases, the payment status channel is closed by the router client after a terminal state (like success) is reached. Since the FSM continues to poll the server for acceptance via PollPaymentAccepted, a closed channel should simply be ignored rather than treated as a failure. Treating it as a failure could abort a swap that has actually succeeded locally but hasn't been acknowledged by the server yet.

		case payRes, ok := <-payChan:
			if !ok {
				payChan = nil
				continue
			}


f.Debugf("payment result: %v", payRes)
if payRes.State == lnrpc.Payment_FAILED {
return f.handleErrorAndUnlockReservations(
ctx, fmt.Errorf("payment failed: %v",
payRes.FailureReason),
)
}
case err := <-paymentErrChan:
case err, ok := <-paymentErrChan:
if !ok {
err = errors.New("payment error channel closed")
} else if err == nil {
err = errors.New("payment error channel returned nil")
}
Comment on lines +261 to +266
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to payChan, the paymentErrChan may be closed normally when the payment stream ends. Treating a closed channel as a terminal failure could abort a swap that has actually succeeded or is still being processed by the server. It is safer to set the channel to nil to stop selecting on it and allow the server polling to determine the final outcome.

Suggested change
case err, ok := <-paymentErrChan:
if !ok {
err = errors.New("payment error channel closed")
} else if err == nil {
err = errors.New("payment error channel returned nil")
}
case err, ok := <-paymentErrChan:
if !ok {
paymentErrChan = nil
continue
} else if err == nil {
err = errors.New("payment error channel returned nil")
}


f.Errorf("error sending payment: %v", err)
return f.handleErrorAndUnlockReservations(ctx, err)

case <-ctx.Done():
return f.handleErrorAndUnlockReservations(ctx, nil)
return f.handleErrorAndUnlockReservations(ctx, ctx.Err())

case <-timer.C:
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
Expand Down Expand Up @@ -616,17 +628,19 @@

// handleErrorAndUnlockReservations handles an error and unlocks the
// reservations.
func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context,

Check failure on line 631 in instantout/actions.go

View workflow job for this annotation

GitHub Actions / build and lint code

(*FSM).handleErrorAndUnlockReservations - ctx is unused (unparam)
err error) fsm.EventType {
// We might get here from a canceled context, we create a new context
// with a timeout to unlock the reservations.
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
unlockCtx, cancel := context.WithTimeout(
context.Background(), time.Second*30,
)
defer cancel()

// Unlock the reservations.
for _, reservation := range f.InstantOut.Reservations {
err := f.cfg.ReservationManager.UnlockReservation(
ctx, reservation.ID,
unlockCtx, reservation.ID,
)
if err != nil {
f.Errorf("error unlocking reservation: %v", err)
Expand All @@ -638,7 +652,9 @@
// release the reservations. This can be done in a goroutine as we
// wan't to fail the fsm early.
go func() {
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
ctx, cancel := context.WithTimeout(
context.Background(), time.Second*30,
)
defer cancel()
_, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap(
ctx, &swapserverrpc.CancelInstantSwapRequest{
Expand Down
12 changes: 12 additions & 0 deletions instantout/instantout.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ func (i *InstantOut) signMusig2Tx(ctx context.Context,
if err != nil {
return nil, err
}
if len(musig2sessions) != len(inputs) {
return nil, fmt.Errorf("invalid number of MuSig2 sessions")
}
if len(counterPartyNonces) != len(inputs) {
return nil, fmt.Errorf("invalid number of peer nonces")
}

prevOutFetcher := inputs.GetPrevoutFetcher()
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
Expand Down Expand Up @@ -329,6 +335,12 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context,
if err != nil {
return nil, err
}
if len(musig2Sessions) != len(inputs) {
return nil, fmt.Errorf("invalid number of MuSig2 sessions")
}
if len(serverSigs) != len(inputs) {
return nil, fmt.Errorf("invalid number of peer signatures")
}

for idx := range inputs {
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
Expand Down
8 changes: 5 additions & 3 deletions instantout/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,12 @@ func (m *Manager) NewInstantOut(ctx context.Context,
protocolVersion: CurrentProtocolVersion(),
sweepAddress: sweepAddr,
}
m.Unlock()

instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation)
if err != nil {
m.Unlock()
return nil, err
}
m.activeInstantOuts[instantOut.InstantOut.SwapHash] = instantOut
m.Unlock()

// Start the instantout FSM.
go func() {
Expand All @@ -186,6 +184,10 @@ func (m *Manager) NewInstantOut(ctx context.Context,
return nil, err
}

m.Lock()
m.activeInstantOuts[instantOut.InstantOut.SwapHash] = instantOut
m.Unlock()

return instantOut, nil
}

Expand Down
Loading