Skip to content

Commit 9c1b745

Browse files
authored
Merge pull request #661 from hieblmi/abandon-swaps
Abandon API for pending Loop-in swaps
2 parents b280753 + ce6f826 commit 9c1b745

21 files changed

+875
-160
lines changed

client.go

Lines changed: 42 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

@@ -374,6 +381,12 @@ func (s *Client) resumeSwaps(ctx context.Context,
374381
continue
375382
}
376383

384+
// Store the swap's abandon channel so that the client can
385+
// abandon the swap by providing the swap hash.
386+
s.executor.Lock()
387+
s.abandonChans[swap.hash] = swap.abandonChan
388+
s.executor.Unlock()
389+
377390
s.executor.initiateSwap(ctx, swap)
378391
}
379392
}
@@ -578,6 +591,10 @@ func (s *Client) LoopIn(globalCtx context.Context,
578591
}
579592
swap := initResult.swap
580593

594+
s.executor.Lock()
595+
s.abandonChans[swap.hash] = swap.abandonChan
596+
s.executor.Unlock()
597+
581598
// Post swap to the main loop.
582599
s.executor.initiateSwap(globalCtx, swap)
583600

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

cmd/loop/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func main() {
147147
monitorCommand, quoteCommand, listAuthCommand,
148148
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
149149
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
150-
getInfoCommand,
150+
getInfoCommand, abandonSwapCommand,
151151
}
152152

153153
err := app.Run(os.Args)

cmd/loop/swaps.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,73 @@ func swapInfo(ctx *cli.Context) error {
9090
printRespJSON(resp)
9191
return nil
9292
}
93+
94+
var abandonSwapCommand = cli.Command{
95+
Name: "abandonswap",
96+
Usage: "abandon a swap with a given swap hash",
97+
Description: "This command overrides the database and abandons a " +
98+
"swap with a given swap hash.\n\n" +
99+
"!!! This command might potentially lead to loss of funds if " +
100+
"it is applied to swaps that are still waiting for pending " +
101+
"user funds. Before executing this command make sure that " +
102+
"no funds are locked by the swap.",
103+
ArgsUsage: "ID",
104+
Flags: []cli.Flag{
105+
cli.BoolFlag{
106+
Name: "i_know_what_i_am_doing",
107+
Usage: "Specify this flag if you made sure that you " +
108+
"read and understood the following " +
109+
"consequence of applying this command.",
110+
},
111+
},
112+
Action: abandonSwap,
113+
}
114+
115+
func abandonSwap(ctx *cli.Context) error {
116+
args := ctx.Args()
117+
118+
var id string
119+
switch {
120+
case ctx.IsSet("id"):
121+
id = ctx.String("id")
122+
123+
case ctx.NArg() > 0:
124+
id = args[0]
125+
args = args.Tail() // nolint:wastedassign
126+
127+
default:
128+
// Show command help if no arguments and flags were provided.
129+
return cli.ShowCommandHelp(ctx, "abandonswap")
130+
}
131+
132+
if len(id) != hex.EncodedLen(lntypes.HashSize) {
133+
return fmt.Errorf("invalid swap ID")
134+
}
135+
idBytes, err := hex.DecodeString(id)
136+
if err != nil {
137+
return fmt.Errorf("cannot hex decode id: %v", err)
138+
}
139+
140+
client, cleanup, err := getClient(ctx)
141+
if err != nil {
142+
return err
143+
}
144+
defer cleanup()
145+
146+
if !ctx.Bool("i_know_what_i_am_doing") {
147+
return cli.ShowCommandHelp(ctx, "abandonswap")
148+
}
149+
150+
resp, err := client.AbandonSwap(
151+
context.Background(), &looprpc.AbandonSwapRequest{
152+
Id: idBytes,
153+
IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
154+
},
155+
)
156+
if err != nil {
157+
return err
158+
}
159+
160+
printRespJSON(resp)
161+
return nil
162+
}

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/perms/perms.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ var RequiredPermissions = map[string][]bakery.Op{
3131
Entity: "swap",
3232
Action: "read",
3333
}},
34+
"/looprpc.SwapClient/AbandonSwap": {{
35+
Entity: "swap",
36+
Action: "execute",
37+
}, {
38+
Entity: "loop",
39+
Action: "in",
40+
}, {
41+
Entity: "loop",
42+
Action: "out",
43+
}},
3444
"/looprpc.SwapClient/LoopOutTerms": {{
3545
Entity: "terms",
3646
Action: "read",

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) {

loopdb/swapstate.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ const (
6464
// StateFailIncorrectHtlcAmt indicates that the amount of an externally
6565
// published loop in htlc didn't match the swap amount.
6666
StateFailIncorrectHtlcAmt SwapState = 10
67+
68+
// StateFailAbandoned indicates that a swap has been abandoned. Its
69+
// execution has been canceled. It won't further be processed.
70+
StateFailAbandoned SwapState = 11
6771
)
6872

6973
// SwapStateType defines the types of swap states that exist. Every swap state
@@ -98,6 +102,18 @@ func (s SwapState) Type() SwapStateType {
98102
return StateTypeFail
99103
}
100104

105+
// IsPending returns true if the swap is in a pending state.
106+
func (s SwapState) IsPending() bool {
107+
return s == StateInitiated || s == StateHtlcPublished ||
108+
s == StatePreimageRevealed || s == StateFailTemporary ||
109+
s == StateInvoiceSettled
110+
}
111+
112+
// IsFinal returns true if the swap is in a final state.
113+
func (s SwapState) IsFinal() bool {
114+
return !s.IsPending()
115+
}
116+
101117
// String returns a string representation of the swap's state.
102118
func (s SwapState) String() string {
103119
switch s {
@@ -134,6 +150,9 @@ func (s SwapState) String() string {
134150
case StateFailIncorrectHtlcAmt:
135151
return "IncorrectHtlcAmt"
136152

153+
case StateFailAbandoned:
154+
return "FailAbandoned"
155+
137156
default:
138157
return "Unknown"
139158
}

0 commit comments

Comments
 (0)