Skip to content

Commit b5d2fb3

Browse files
committed
loopd: add loop in state InvoiceSettled
1 parent a7f4feb commit b5d2fb3

File tree

7 files changed

+234
-182
lines changed

7 files changed

+234
-182
lines changed

cmd/loopd/swapclient_server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
9595
state = looprpc.SwapState_PREIMAGE_REVEALED
9696
case loopdb.StateHtlcPublished:
9797
state = looprpc.SwapState_HTLC_PUBLISHED
98+
case loopdb.StateInvoiceSettled:
99+
state = looprpc.SwapState_INVOICE_SETTLED
98100
case loopdb.StateSuccess:
99101
state = looprpc.SwapState_SUCCESS
100102
default:

loopdb/swapstate.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const (
5151

5252
// StateHtlcPublished means that the client published the on-chain htlc.
5353
StateHtlcPublished = 8
54+
55+
// StateInvoiceSettled means that the swap invoice has been paid by the
56+
// server.
57+
StateInvoiceSettled = 9
5458
)
5559

5660
// SwapStateType defines the types of swap states that exist. Every swap state
@@ -114,6 +118,9 @@ func (s SwapState) String() string {
114118
case StateFailTemporary:
115119
return "FailTemporary"
116120

121+
case StateInvoiceSettled:
122+
return "InvoiceSettled"
123+
117124
default:
118125
return "Unknown"
119126
}

loopin.go

Lines changed: 98 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ func (s *loopInSwap) execute(mainCtx context.Context,
252252
// permanently and losing funds.
253253
if err != nil {
254254
s.log.Errorf("Swap error: %v", err)
255-
s.state = loopdb.StateFailTemporary
255+
s.setState(loopdb.StateFailTemporary)
256256

257257
// If we cannot send out this update, there is nothing we can do.
258258
_ = s.sendUpdate(mainCtx)
@@ -282,7 +282,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
282282
// If an external htlc was indicated, we can move to the
283283
// HtlcPublished state directly and wait for
284284
// confirmation.
285-
s.state = loopdb.StateHtlcPublished
285+
s.setState(loopdb.StateHtlcPublished)
286286
err = s.persistState(globalCtx)
287287
if err != nil {
288288
return err
@@ -320,41 +320,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
320320
// invoice, receive the preimage and sweep the htlc. We are waiting for
321321
// this to happen and simultaneously watch the htlc expiry height. When
322322
// the htlc expires, we will publish a timeout tx to reclaim the funds.
323-
spend, err := s.waitForHtlcSpend(globalCtx, htlcOutpoint)
324-
if err != nil {
325-
return err
326-
}
327-
328-
// Determine the htlc input of the spending tx and inspect the witness
329-
// to findout whether a success or a timeout tx spend the htlc.
330-
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
331-
332-
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
333-
s.state = loopdb.StateSuccess
334-
335-
// Server swept the htlc. The htlc value can be added to the
336-
// server cost balance.
337-
s.cost.Server += htlcValue
338-
} else {
339-
s.state = loopdb.StateFailTimeout
340-
341-
// Now that the timeout tx confirmed, we can safely cancel the
342-
// swap invoice. We still need to query the final invoice state.
343-
// This is not a hodl invoice, so it may be that the invoice was
344-
// already settled. This means that the server didn't succeed in
345-
// sweeping the htlc after paying the invoice.
346-
err := s.lnd.Invoices.CancelInvoice(globalCtx, s.hash)
347-
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
348-
return err
349-
}
350-
351-
// TODO: Add miner fee of timeout tx to swap cost balance.
352-
}
353-
354-
// Wait for a final state of the swap invoice. It should either be
355-
// settled because the server successfully paid it or canceled because
356-
// we canceled after our timeout tx confirmed.
357-
err = s.waitForSwapInvoiceResult(globalCtx)
323+
err = s.waitForSwapComplete(globalCtx, htlcOutpoint, htlcValue)
358324
if err != nil {
359325
return err
360326
}
@@ -411,7 +377,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
411377

412378
// Verify whether it still makes sense to publish the htlc.
413379
if blocksRemaining < MinLoopInPublishDelta {
414-
s.state = loopdb.StateFailTimeout
380+
s.setState(loopdb.StateFailTimeout)
415381
return false, s.persistState(ctx)
416382
}
417383

@@ -425,7 +391,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
425391

426392
// Transition to state HtlcPublished before calling SendOutputs to
427393
// prevent us from ever paying multiple times after a crash.
428-
s.state = loopdb.StateHtlcPublished
394+
s.setState(loopdb.StateHtlcPublished)
429395
err = s.persistState(ctx)
430396
if err != nil {
431397
return false, err
@@ -448,10 +414,11 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
448414

449415
}
450416

451-
// waitForHtlcSpend waits until a spending tx of the htlc gets confirmed and
452-
// returns the spend details.
453-
func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
454-
htlc *wire.OutPoint) (*chainntnfs.SpendDetail, error) {
417+
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
418+
// the swap invoice is either settled or canceled. If the htlc times out, the
419+
// timeout tx will be published.
420+
func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
421+
htlc *wire.OutPoint, htlcValue btcutil.Amount) error {
455422

456423
// Register the htlc spend notification.
457424
rpcCtx, cancel := context.WithCancel(ctx)
@@ -460,58 +427,61 @@ func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
460427
rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight,
461428
)
462429
if err != nil {
463-
return nil, fmt.Errorf("register spend ntfn: %v", err)
430+
return fmt.Errorf("register spend ntfn: %v", err)
464431
}
465432

466-
for {
433+
// Register for swap invoice updates.
434+
rpcCtx, cancel = context.WithCancel(ctx)
435+
defer cancel()
436+
s.log.Infof("Subscribing to swap invoice %v", s.hash)
437+
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
438+
rpcCtx, s.hash,
439+
)
440+
if err != nil {
441+
return fmt.Errorf("subscribe to swap invoice: %v", err)
442+
}
443+
444+
htlcSpend := false
445+
invoiceFinalized := false
446+
for !htlcSpend || !invoiceFinalized {
467447
select {
468448
// Spend notification error.
469449
case err := <-spendErr:
470-
return nil, err
450+
return err
471451

452+
// Receive block epochs and start publishing the timeout tx
453+
// whenever possible.
472454
case notification := <-s.blockEpochChan:
473455
s.height = notification.(int32)
474456

475457
if s.height >= s.LoopInContract.CltvExpiry {
476458
err := s.publishTimeoutTx(ctx, htlc)
477459
if err != nil {
478-
return nil, err
460+
return err
479461
}
480462
}
481463

482-
// Htlc spend, break loop.
464+
// The htlc spend is confirmed. Inspect the spending tx to
465+
// determine the final swap state.
483466
case spendDetails := <-spendChan:
484467
s.log.Infof("Htlc spend by tx: %v",
485468
spendDetails.SpenderTxHash)
486469

487-
return spendDetails, nil
488-
489-
case <-ctx.Done():
490-
return nil, ctx.Err()
491-
}
492-
}
493-
}
494-
495-
// waitForSwapPaid waits until our swap invoice gets paid by the server.
496-
func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
497-
// Wait for swap invoice to be paid.
498-
rpcCtx, cancel := context.WithCancel(ctx)
499-
defer cancel()
500-
s.log.Infof("Subscribing to swap invoice %v", s.hash)
501-
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
502-
rpcCtx, s.hash,
503-
)
504-
if err != nil {
505-
return err
506-
}
470+
err := s.processHtlcSpend(
471+
ctx, spendDetails, htlcValue,
472+
)
473+
if err != nil {
474+
return err
475+
}
507476

508-
for {
509-
select {
477+
htlcSpend = true
510478

511479
// Swap invoice ntfn error.
512480
case err := <-swapInvoiceErr:
513481
return err
514482

483+
// An update to the swap invoice occured. Check the new state
484+
// and update the swap state accordingly.
515485
case update := <-swapInvoiceChan:
516486
s.log.Infof("Received swap invoice update: %v",
517487
update.State)
@@ -521,18 +491,67 @@ func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
521491
// Swap invoice was paid, so update server cost balance.
522492
case channeldb.ContractSettled:
523493
s.cost.Server -= update.AmtPaid
524-
return nil
494+
495+
// If invoice settlement and htlc spend happen
496+
// in the expected order, move the swap to an
497+
// intermediate state that indicates that the
498+
// swap is complete from the user point of view,
499+
// but still incomplete with regards to
500+
// accounting data.
501+
if s.state == loopdb.StateHtlcPublished {
502+
s.setState(loopdb.StateInvoiceSettled)
503+
err := s.persistState(ctx)
504+
if err != nil {
505+
return err
506+
}
507+
}
508+
509+
invoiceFinalized = true
525510

526511
// Canceled invoice has no effect on server cost
527512
// balance.
528513
case channeldb.ContractCanceled:
529-
return nil
514+
invoiceFinalized = true
530515
}
531516

532517
case <-ctx.Done():
533518
return ctx.Err()
534519
}
535520
}
521+
522+
return nil
523+
}
524+
525+
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
526+
spend *chainntnfs.SpendDetail, htlcValue btcutil.Amount) error {
527+
528+
// Determine the htlc input of the spending tx and inspect the witness
529+
// to findout whether a success or a timeout tx spend the htlc.
530+
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
531+
532+
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
533+
s.setState(loopdb.StateSuccess)
534+
535+
// Server swept the htlc. The htlc value can be added to the
536+
// server cost balance.
537+
s.cost.Server += htlcValue
538+
} else {
539+
s.setState(loopdb.StateFailTimeout)
540+
541+
// Now that the timeout tx confirmed, we can safely cancel the
542+
// swap invoice. We still need to query the final invoice state.
543+
// This is not a hodl invoice, so it may be that the invoice was
544+
// already settled. This means that the server didn't succeed in
545+
// sweeping the htlc after paying the invoice.
546+
err := s.lnd.Invoices.CancelInvoice(ctx, s.hash)
547+
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
548+
return err
549+
}
550+
551+
// TODO: Add miner fee of timeout tx to swap cost balance.
552+
}
553+
554+
return nil
536555
}
537556

538557
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
@@ -582,16 +601,18 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
582601

583602
// persistState updates the swap state and sends out an update notification.
584603
func (s *loopInSwap) persistState(ctx context.Context) error {
585-
updateTime := time.Now()
586-
587-
s.lastUpdateTime = updateTime
588-
589604
// Update state in store.
590-
err := s.store.UpdateLoopIn(s.hash, updateTime, s.state)
605+
err := s.store.UpdateLoopIn(s.hash, s.lastUpdateTime, s.state)
591606
if err != nil {
592607
return err
593608
}
594609

595610
// Send out swap update
596611
return s.sendUpdate(ctx)
597612
}
613+
614+
// setState updates the swap state and last update timestamp.
615+
func (s *loopInSwap) setState(state loopdb.SwapState) {
616+
s.lastUpdateTime = time.Now()
617+
s.state = state
618+
}

0 commit comments

Comments
 (0)