mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
instantout: unlock reservations on OnRecover from in-flight states
SendPaymentAndPollAccepted and BuildHtlc both run after PollPaymentAcceptedAction has called LockReservation on every reservation backing the swap. Their OnRecover transitions pointed directly to Failed, whose action is fsm.NoOpAction -- so on daemon restart while in either state, the FSM moved to Failed without ever unlocking the reservations. The local store kept them in the Locked state until on-chain expiry (typically tens of hours later), making them unusable for any subsequent swap. For users who pay for reservations (PR #883's invoice-requested flow) that is a direct material loss. Add an intermediate UnlockReservationsOnRecover state whose action calls handleErrorAndUnlockReservations and then routes to Failed via the normal OnError edge. SendPaymentAndPollAccepted.OnRecover and BuildHtlc.OnRecover now point at this state instead of Failed directly. Init.OnRecover -> Failed is left alone because at that point the InstantOut row has not yet been persisted and no reservation locks have been taken; there is nothing to clean up. Post-PushPreimage states (PushPreimage.OnRecover -> PushPreimage, etc.) are also left alone since they self-loop on recovery rather than terminate. The cleanup helper itself still derives its context from the caller's context (see existing handleErrorAndUnlockReservations); fixing that context-cancel hazard is a separate change.
This commit is contained in:
parent
17df6e86e6
commit
10512bfa19
2 changed files with 37 additions and 2 deletions
|
|
@ -691,6 +691,21 @@ func (f *FSM) WaitForHtlcSweepConfirmedAction(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
// unlockReservationsOnRecoverAction is the action of the
|
||||
// UnlockReservationsOnRecover state. It is entered via OnRecover from any
|
||||
// in-flight state where the reservations are already locked, and it unlocks
|
||||
// them before routing to Failed. Without this, a crash between
|
||||
// PollPaymentAcceptedAction's LockReservation and the swap reaching a
|
||||
// terminal state would leave the reservations permanently Locked in the
|
||||
// local store, blocking any future InstantOut that wants to spend them.
|
||||
func (f *FSM) unlockReservationsOnRecoverAction(ctx context.Context,
|
||||
_ fsm.EventContext) fsm.EventType {
|
||||
|
||||
return f.handleErrorAndUnlockReservations(
|
||||
ctx, errors.New("instant out recovered from in-flight state"),
|
||||
)
|
||||
}
|
||||
|
||||
// handleErrorAndUnlockReservations handles an error and unlocks the
|
||||
// reservations.
|
||||
func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,14 @@ var (
|
|||
// FailedHtlcSweep is the state where the htlc sweep failed.
|
||||
FailedHtlcSweep = fsm.StateType("FailedHtlcSweep")
|
||||
|
||||
// UnlockReservationsOnRecover is a transient state entered via
|
||||
// OnRecover from any in-flight state that had already locked the
|
||||
// underlying reservations. Its action unlocks them and routes the
|
||||
// FSM to Failed, so a crash mid-swap does not leave reservations
|
||||
// stuck Locked in the local store.
|
||||
UnlockReservationsOnRecover = fsm.StateType(
|
||||
"UnlockReservationsOnRecover")
|
||||
|
||||
// Failed is the state where the swap failed.
|
||||
Failed = fsm.StateType("InstantOutFailed")
|
||||
)
|
||||
|
|
@ -246,7 +254,11 @@ func (f *FSM) GetV1ReservationStates() fsm.States {
|
|||
Transitions: fsm.Transitions{
|
||||
OnPaymentAccepted: BuildHtlc,
|
||||
fsm.OnError: Failed,
|
||||
OnRecover: Failed,
|
||||
// OnRecover must go through cleanup since
|
||||
// PollPaymentAcceptedAction has already locked
|
||||
// the reservations by the time the FSM can
|
||||
// crash here.
|
||||
OnRecover: UnlockReservationsOnRecover,
|
||||
},
|
||||
Action: f.PollPaymentAcceptedAction,
|
||||
},
|
||||
|
|
@ -254,10 +266,18 @@ func (f *FSM) GetV1ReservationStates() fsm.States {
|
|||
Transitions: fsm.Transitions{
|
||||
OnHtlcSigReceived: PushPreimage,
|
||||
fsm.OnError: Failed,
|
||||
OnRecover: Failed,
|
||||
// Same as SendPaymentAndPollAccepted -- the
|
||||
// reservations are still locked at this point.
|
||||
OnRecover: UnlockReservationsOnRecover,
|
||||
},
|
||||
Action: f.BuildHTLCAction,
|
||||
},
|
||||
UnlockReservationsOnRecover: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
fsm.OnError: Failed,
|
||||
},
|
||||
Action: f.unlockReservationsOnRecoverAction,
|
||||
},
|
||||
PushPreimage: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnSweeplessSweepPublished: WaitForSweeplessSweepConfirmed,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue