Skip to content

Commit 5e91c44

Browse files
committed
loopd: abandon loop-ins
1 parent 0fbf253 commit 5e91c44

File tree

6 files changed

+161
-3
lines changed

6 files changed

+161
-3
lines changed

client.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightninglabs/loop/loopdb"
1818
"github.com/lightninglabs/loop/swap"
1919
"github.com/lightninglabs/loop/sweep"
20+
"github.com/lightningnetwork/lnd/lntypes"
2021
"github.com/lightningnetwork/lnd/routing/route"
2122
"google.golang.org/grpc/status"
2223
)
@@ -68,6 +69,11 @@ type Client struct {
6869
started uint32 // To be used atomically.
6970
errChan chan error
7071

72+
// abandonChans allows for accessing a swap's abandon channel by
73+
// providing its swap hash. This map is used to look up the abandon
74+
// channel of a swap if the client requests to abandon it.
75+
abandonChans map[lntypes.Hash]chan struct{}
76+
7177
lndServices *lndclient.LndServices
7278
sweeper *sweep.Sweeper
7379
executor *executor
@@ -179,6 +185,7 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
179185
sweeper: sweeper,
180186
executor: executor,
181187
resumeReady: make(chan struct{}),
188+
abandonChans: make(map[lntypes.Hash]chan struct{}),
182189
}
183190

184191
cleanup := func() {
@@ -317,10 +324,10 @@ func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
317324
}()
318325

319326
// Main event loop.
320-
err = s.executor.run(mainCtx, statusChan)
327+
err = s.executor.run(mainCtx, statusChan, s.abandonChans)
321328

322329
// Consider canceled as happy flow.
323-
if err == context.Canceled {
330+
if errors.Is(err, context.Canceled) {
324331
err = nil
325332
}
326333

@@ -578,6 +585,10 @@ func (s *Client) LoopIn(globalCtx context.Context,
578585
}
579586
swap := initResult.swap
580587

588+
s.executor.Lock()
589+
s.abandonChans[swap.hash] = swap.abandonChan
590+
s.executor.Unlock()
591+
581592
// Post swap to the main loop.
582593
s.executor.initiateSwap(globalCtx, swap)
583594

@@ -753,3 +764,26 @@ func (s *Client) Probe(ctx context.Context, req *ProbeRequest) error {
753764
req.RouteHints,
754765
)
755766
}
767+
768+
// AbandonSwap sends a signal on the abandon channel of the swap identified by
769+
// the passed swap hash. This will cause the swap to abandon itself.
770+
func (s *Client) AbandonSwap(ctx context.Context,
771+
req *AbandonSwapRequest) error {
772+
773+
if req == nil {
774+
return errors.New("no request provided")
775+
}
776+
777+
s.executor.Lock()
778+
defer s.executor.Unlock()
779+
780+
select {
781+
case s.abandonChans[req.SwapHash] <- struct{}{}:
782+
case <-ctx.Done():
783+
return ctx.Err()
784+
default:
785+
// This is to avoid writing to a full channel.
786+
}
787+
788+
return nil
789+
}

executor.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/lightninglabs/lndclient"
1414
"github.com/lightninglabs/loop/loopdb"
1515
"github.com/lightninglabs/loop/sweep"
16+
"github.com/lightningnetwork/lnd/lntypes"
1617
"github.com/lightningnetwork/lnd/queue"
1718
)
1819

@@ -46,6 +47,8 @@ type executor struct {
4647
currentHeight uint32
4748
ready chan struct{}
4849

50+
sync.Mutex
51+
4952
executorConfig
5053
}
5154

@@ -61,7 +64,8 @@ func newExecutor(cfg *executorConfig) *executor {
6164
// run starts the executor event loop. It accepts and executes new swaps,
6265
// providing them with required config data.
6366
func (s *executor) run(mainCtx context.Context,
64-
statusChan chan<- SwapInfo) error {
67+
statusChan chan<- SwapInfo,
68+
abandonChans map[lntypes.Hash]chan struct{}) error {
6569

6670
var (
6771
err error
@@ -167,6 +171,15 @@ func (s *executor) run(mainCtx context.Context,
167171
log.Errorf("Execute error: %v", err)
168172
}
169173

174+
// If a loop-in ended we have to remove its
175+
// abandon channel from our abandonChans map
176+
// since the swap finalized.
177+
if swap, ok := newSwap.(*loopInSwap); ok {
178+
s.Lock()
179+
delete(abandonChans, swap.hash)
180+
s.Unlock()
181+
}
182+
170183
select {
171184
case swapDoneChan <- swapID:
172185
case <-mainCtx.Done():

interface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,9 @@ type ProbeRequest struct {
394394
// Optional hop hints.
395395
RouteHints [][]zpay32.HopHint
396396
}
397+
398+
// AbandonSwapRequest specifies the swap to abandon. It is identified by its
399+
// swap hash.
400+
type AbandonSwapRequest struct {
401+
SwapHash lntypes.Hash
402+
}

loopd/swapclient_server.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
288288
case loopdb.StateFailIncorrectHtlcAmt:
289289
failureReason = clientrpc.FailureReason_FAILURE_REASON_INCORRECT_AMOUNT
290290

291+
case loopdb.StateFailAbandoned:
292+
failureReason = clientrpc.FailureReason_FAILURE_REASON_ABANDONED
293+
291294
default:
292295
return nil, fmt.Errorf("unknown swap state: %v", loopSwap.State)
293296
}
@@ -508,6 +511,49 @@ func (s *swapClientServer) SwapInfo(_ context.Context,
508511
return s.marshallSwap(&swp)
509512
}
510513

514+
// AbandonSwap requests the server to abandon a swap with the given hash.
515+
func (s *swapClientServer) AbandonSwap(ctx context.Context,
516+
req *clientrpc.AbandonSwapRequest) (*clientrpc.AbandonSwapResponse,
517+
error) {
518+
519+
if !req.IKnowWhatIAmDoing {
520+
return nil, fmt.Errorf("please read the AbandonSwap API " +
521+
"documentation")
522+
}
523+
524+
swapHash, err := lntypes.MakeHash(req.Id)
525+
if err != nil {
526+
return nil, fmt.Errorf("error parsing swap hash: %v", err)
527+
}
528+
529+
s.swapsLock.Lock()
530+
swap, ok := s.swaps[swapHash]
531+
s.swapsLock.Unlock()
532+
if !ok {
533+
return nil, fmt.Errorf("swap with hash %s not found", req.Id)
534+
}
535+
536+
if swap.SwapType.IsOut() {
537+
return nil, fmt.Errorf("abandoning loop out swaps is not " +
538+
"supported yet")
539+
}
540+
541+
// If the swap is in a final state, we cannot abandon it.
542+
if swap.State.IsFinal() {
543+
return nil, fmt.Errorf("cannot abandon swap in final state, "+
544+
"state = %s, hash = %s", swap.State.String(), swapHash)
545+
}
546+
547+
err = s.impl.AbandonSwap(ctx, &loop.AbandonSwapRequest{
548+
SwapHash: swapHash,
549+
})
550+
if err != nil {
551+
return nil, fmt.Errorf("error abandoning swap: %v", err)
552+
}
553+
554+
return &clientrpc.AbandonSwapResponse{}, nil
555+
}
556+
511557
// LoopOutTerms returns the terms that the server enforces for loop out swaps.
512558
func (s *swapClientServer) LoopOutTerms(ctx context.Context,
513559
_ *clientrpc.TermsRequest) (*clientrpc.OutTermsResponse, error) {

loopin.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ var (
4848
// TimeoutTxConfTarget defines the confirmation target for the loop in
4949
// timeout tx.
5050
TimeoutTxConfTarget = int32(2)
51+
52+
// ErrSwapFinalized is returned when a to be executed swap is already in
53+
// a final state.
54+
ErrSwapFinalized = errors.New("swap is in a final state")
5155
)
5256

5357
// loopInSwap contains all the in-memory state related to a pending loop in
@@ -70,6 +74,8 @@ type loopInSwap struct {
7074

7175
timeoutAddr btcutil.Address
7276

77+
abandonChan chan struct{}
78+
7379
wg sync.WaitGroup
7480
}
7581

@@ -308,6 +314,8 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
308314
swap.log.Infof("Server message: %v", swapResp.serverMessage)
309315
}
310316

317+
swap.abandonChan = make(chan struct{}, 1)
318+
311319
return &loopInInitResult{
312320
swap: swap,
313321
serverMessage: swapResp.serverMessage,
@@ -518,6 +526,11 @@ func (s *loopInSwap) execute(mainCtx context.Context,
518526
// error occurs.
519527
err = s.executeSwap(mainCtx)
520528

529+
// Stop the execution if the swap has been abandoned.
530+
if err != nil && s.state == loopdb.StateFailAbandoned {
531+
return err
532+
}
533+
521534
// Sanity check. If there is no error, the swap must be in a final
522535
// state.
523536
if err == nil && s.state.Type() == loopdb.StateTypePending {
@@ -553,6 +566,11 @@ func (s *loopInSwap) execute(mainCtx context.Context,
553566
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
554567
var err error
555568

569+
// If the swap is already in a final state, we can return immediately.
570+
if s.state.IsFinal() {
571+
return ErrSwapFinalized
572+
}
573+
556574
// For loop in, the client takes the first step by publishing the
557575
// on-chain htlc. Only do this if we haven't already done so in a
558576
// previous run.
@@ -688,6 +706,11 @@ func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
688706
case notification := <-s.blockEpochChan:
689707
s.height = notification.(int32)
690708

709+
// If the client requested the swap to be abandoned, we override
710+
// the status in the database.
711+
case <-s.abandonChan:
712+
return nil, s.setStateAbandoned(ctx)
713+
691714
// Cancel.
692715
case <-globalCtx.Done():
693716
return nil, globalCtx.Err()
@@ -840,6 +863,11 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
840863
htlcKeyRevealed := false
841864
for !htlcSpend || !invoiceFinalized {
842865
select {
866+
// If the client requested the swap to be abandoned, we override
867+
// the status in the database.
868+
case <-s.abandonChan:
869+
return s.setStateAbandoned(ctx)
870+
843871
// Spend notification error.
844872
case err := <-spendErr:
845873
return err
@@ -1062,6 +1090,31 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
10621090
return fee, nil
10631091
}
10641092

1093+
// setStateAbandoned stores the abandoned state and announces it. It also
1094+
// cancels the swap invoice so the server can't settle it.
1095+
func (s *loopInSwap) setStateAbandoned(ctx context.Context) error {
1096+
s.log.Infof("Abandoning swap %v...", s.hash)
1097+
1098+
if !s.state.IsPending() {
1099+
return fmt.Errorf("cannot abandon swap in state %v", s.state)
1100+
}
1101+
1102+
s.setState(loopdb.StateFailAbandoned)
1103+
1104+
err := s.persistAndAnnounceState(ctx)
1105+
if err != nil {
1106+
return err
1107+
}
1108+
1109+
// If the invoice is already settled or canceled, this is a nop.
1110+
_ = s.lnd.Invoices.CancelInvoice(ctx, s.hash)
1111+
1112+
return fmt.Errorf("swap hash "+
1113+
"abandoned by client, "+
1114+
"swap ID: %v, %v",
1115+
s.hash, err)
1116+
}
1117+
10651118
// persistAndAnnounceState updates the swap state on disk and sends out an
10661119
// update notification.
10671120
func (s *loopInSwap) persistAndAnnounceState(ctx context.Context) error {

swap/type.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const (
1111
TypeOut
1212
)
1313

14+
// IsOut returns true if the swap is a loop out swap, false if it is a loop in
15+
// swap.
16+
func (t Type) IsOut() bool {
17+
return t == TypeOut
18+
}
19+
1420
func (t Type) String() string {
1521
switch t {
1622
case TypeIn:

0 commit comments

Comments
 (0)