From 1a31bbf75df78655b52428cc0d04610937e77529 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Thu, 8 Feb 2024 15:55:05 +0100 Subject: [PATCH 1/2] fsm: add early abort observer option --- fsm/fsm.go | 14 +++++++++---- fsm/observer.go | 52 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/fsm/fsm.go b/fsm/fsm.go index f1f1649..6d36812 100644 --- a/fsm/fsm.go +++ b/fsm/fsm.go @@ -13,7 +13,10 @@ var ( ErrWaitForStateTimedOut = errors.New( "timed out while waiting for event", ) - ErrInvalidContextType = errors.New("invalid context") + ErrInvalidContextType = errors.New("invalid context") + ErrWaitingForStateEarlyAbortError = errors.New( + "waiting for state early abort", + ) ) const ( @@ -73,6 +76,8 @@ type Notification struct { NextState StateType // Event is the event that was processed. Event EventType + // LastActionError is the error returned by the last action executed. + LastActionError error } // Observer is an interface that can be implemented by types that want to @@ -214,9 +219,10 @@ func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error { // Notify the state machine's observers. s.observerMutex.Lock() notification := Notification{ - PreviousState: s.previous, - NextState: s.current, - Event: event, + PreviousState: s.previous, + NextState: s.current, + Event: event, + LastActionError: s.LastActionError, } for _, observer := range s.observers { diff --git a/fsm/observer.go b/fsm/observer.go index 2d5e3fc..8677d51 100644 --- a/fsm/observer.go +++ b/fsm/observer.go @@ -55,7 +55,8 @@ type WaitForStateOption interface { // fsmOptions is a struct that holds all options that can be passed to the // WaitForState function. type fsmOptions struct { - initialWait time.Duration + initialWait time.Duration + abortEarlyOnError bool } // InitialWaitOption is an option that can be passed to the WaitForState @@ -76,6 +77,24 @@ func (w *InitialWaitOption) apply(o *fsmOptions) { o.initialWait = w.initialWait } +// AbortEarlyOnErrorOption is an option that can be passed to the WaitForState +// function to abort early if an error occurs. +type AbortEarlyOnErrorOption struct { + abortEarlyOnError bool +} + +// apply implements the WaitForStateOption interface. +func (a *AbortEarlyOnErrorOption) apply(o *fsmOptions) { + o.abortEarlyOnError = a.abortEarlyOnError +} + +// WithAbortEarlyOnErrorOption creates a new AbortEarlyOnErrorOption. +func WithAbortEarlyOnErrorOption() WaitForStateOption { + return &AbortEarlyOnErrorOption{ + abortEarlyOnError: true, + } +} + // WaitForState waits for the state machine to reach the given state. // If the optional initialWait parameter is set, the function will wait for // the given duration before checking the state. This is useful if the @@ -105,7 +124,8 @@ func (s *CachedObserver) WaitForState(ctx context.Context, defer cancel() // Channel to notify when the desired state is reached - ch := make(chan struct{}) + // or an error occurred. + ch := make(chan error) // Goroutine to wait on condition variable go func() { @@ -115,8 +135,26 @@ func (s *CachedObserver) WaitForState(ctx context.Context, for { // Check if the last state is the desired state if s.lastNotification.NextState == state { - ch <- struct{}{} - return + select { + case <-timeoutCtx.Done(): + return + + case ch <- nil: + return + } + } + + // Check if an error occurred + if s.lastNotification.Event == OnError { + if options.abortEarlyOnError { + select { + case <-timeoutCtx.Done(): + return + + case ch <- s.lastNotification.LastActionError: + return + } + } } // Otherwise, wait for the next notification @@ -130,7 +168,11 @@ func (s *CachedObserver) WaitForState(ctx context.Context, return NewErrWaitingForStateTimeout( state, s.lastNotification.NextState, ) - case <-ch: + + case lastActionErr := <-ch: + if lastActionErr != nil { + return lastActionErr + } return nil } } From a7ab998a3bf558bfdf74fcbd7c6918bd95b8a2b4 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Thu, 8 Feb 2024 16:30:52 +0100 Subject: [PATCH 2/2] instantout: check reservation expiry --- instantout/actions.go | 15 ++++++++++++++- instantout/manager.go | 10 +++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/instantout/actions.go b/instantout/actions.go index 46e53af..d0c8f1b 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -21,7 +21,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" ) -var ( +const ( // Define route independent max routing fees. We have currently no way // to get a reliable estimate of the routing fees. Best we can do is // the minimum routing fees, which is not very indicative. @@ -46,6 +46,10 @@ var ( // defaultPollPaymentTime is the default time to poll the server for the // payment status. defaultPollPaymentTime = time.Second * 15 + + // htlcExpiryDelta is the delta in blocks we require between the htlc + // expiry and reservation expiry. + htlcExpiryDelta = int32(40) ) // InitInstantOutCtx contains the context for the InitInstantOutAction. @@ -96,6 +100,15 @@ func (f *FSM) InitInstantOutAction(eventCtx fsm.EventContext) fsm.EventType { reservationAmt += uint64(res.Value) reservationIds = append(reservationIds, resId[:]) reservations = append(reservations, res) + + // Check that the reservation expiry is larger than the cltv + // expiry of the swap, with an additional delta to allow for + // preimage reveal. + if int32(res.Expiry) < initCtx.cltvExpiry+htlcExpiryDelta { + return f.HandleError(fmt.Errorf("reservation %x has "+ + "expiry %v which is less than the swap expiry %v", + resId, res.Expiry, initCtx.cltvExpiry)) + } } // Create the preimage for the swap. diff --git a/instantout/manager.go b/instantout/manager.go index 1de7f5c..b3e74e6 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -8,6 +8,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightningnetwork/lnd/lntypes" @@ -169,15 +170,10 @@ func (m *Manager) NewInstantOut(ctx context.Context, // waiting for sweepless sweep to be confirmed. err = instantOut.DefaultObserver.WaitForState( ctx, defaultStateWaitTime, WaitForSweeplessSweepConfirmed, + fsm.WithAbortEarlyOnErrorOption(), ) if err != nil { - if instantOut.LastActionError != nil { - return instantOut, fmt.Errorf( - "error waiting for sweepless sweep "+ - "confirmed: %w", instantOut.LastActionError, - ) - } - return instantOut, nil + return nil, err } return instantOut, nil