Skip to content

Commit 245e6b7

Browse files
authored
Merge pull request #372 from carlaKC/sweep-abandon
loopout: do not reveal preimage too close to expiry
2 parents db56b31 + 9db8bd5 commit 245e6b7

File tree

4 files changed

+182
-29
lines changed

4 files changed

+182
-29
lines changed

loopout.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,12 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
438438
return err
439439
}
440440

441+
// If spend details are nil, we resolved the swap without waiting for
442+
// its spend, so we can exit.
443+
if spendDetails == nil {
444+
return nil
445+
}
446+
441447
// Inspect witness stack to see if it is a success transaction. We
442448
// don't just try to match with the hash of our sweep tx, because it
443449
// may be swept by a different (fee) sweep tx from a previous run.
@@ -854,6 +860,14 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
854860
return nil, err
855861
}
856862

863+
// If the result of our spend func was that the swap
864+
// has reached a final state, then we return nil spend
865+
// details, because there is no further action required
866+
// for this swap.
867+
if s.state.Type() != loopdb.StateTypePending {
868+
return nil, nil
869+
}
870+
857871
// If our off chain payment is not yet complete, we
858872
// try to push our preimage to the server.
859873
if !paymentComplete {
@@ -889,7 +903,9 @@ func (s *loopOutSwap) pushPreimage(ctx context.Context) {
889903

890904
// sweep tries to sweep the given htlc to a destination address. It takes into
891905
// account the max miner fee and marks the preimage as revealed when it
892-
// published the tx.
906+
// published the tx. If the preimage has not yet been revealed, and the time
907+
// during which we can safely reveal it has passed, the swap will be marked
908+
// as failed, and the function will return.
893909
//
894910
// TODO: Use lnd sweeper?
895911
func (s *loopOutSwap) sweep(ctx context.Context,
@@ -900,16 +916,36 @@ func (s *loopOutSwap) sweep(ctx context.Context,
900916
return s.htlc.GenSuccessWitness(sig, s.Preimage)
901917
}
902918

919+
remainingBlocks := s.CltvExpiry - s.height
920+
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
921+
preimageRevealed := s.state == loopdb.StatePreimageRevealed
922+
923+
// If we have not revealed our preimage, and we don't have time left
924+
// to sweep the swap, we abandon the swap because we can no longer
925+
// sweep the success path (without potentially having to compete with
926+
// the server's timeout sweep), and we have not had any coins pulled
927+
// off-chain.
928+
if blocksToLastReveal <= 0 && !preimageRevealed {
929+
s.log.Infof("Preimage can no longer be safely revealed: "+
930+
"expires at: %v, current height: %v", s.CltvExpiry,
931+
s.height)
932+
933+
s.state = loopdb.StateFailTimeout
934+
return nil
935+
}
936+
903937
// Calculate the transaction fee based on the confirmation target
904938
// required to sweep the HTLC before the timeout. We'll use the
905939
// confirmation target provided by the client unless we've come too
906940
// close to the expiration height, in which case we'll use the default
907941
// if it is better than what the client provided.
908942
confTarget := s.SweepConfTarget
909-
if s.CltvExpiry-s.height <= DefaultSweepConfTargetDelta &&
943+
if remainingBlocks <= DefaultSweepConfTargetDelta &&
910944
confTarget > DefaultSweepConfTarget {
945+
911946
confTarget = DefaultSweepConfTarget
912947
}
948+
913949
fee, err := s.sweeper.GetSweepFee(
914950
ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget,
915951
)
@@ -922,7 +958,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
922958
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
923959
fee, s.MaxMinerFee)
924960

925-
if s.state == loopdb.StatePreimageRevealed {
961+
if preimageRevealed {
926962
// The currently required fee exceeds the max, but we
927963
// already revealed the preimage. The best we can do now
928964
// is to republish with the max fee.

loopout_test.go

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -414,23 +414,16 @@ func TestCustomSweepConfTarget(t *testing.T) {
414414
// not detected our settle) and settle the off chain htlc, indicating that the
415415
// server successfully settled using the preimage push. In this test, we need
416416
// to start with a fee rate that will be too high, then progress to an
417-
// acceptable one. We do this by starting with a high confirmation target with
418-
// a high fee, and setting the default confirmation fee (which our swap will
419-
// drop down to if it is not confirming in time) to a lower fee. This is not
420-
// intuitive (lower confs having lower fees), but it allows up to mock fee
421-
// changes.
417+
// acceptable one.
422418
func TestPreimagePush(t *testing.T) {
423419
defer test.Guard(t)()
424420

425421
lnd := test.NewMockLnd()
426422
ctx := test.NewContext(t, lnd)
427423
server := newServerMock(lnd)
428424

429-
// Start with a high confirmation delta which will have a very high fee
430-
// attached to it.
431425
testReq := *testRequest
432-
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
433-
DefaultSweepConfTargetDelta - 1
426+
testReq.SweepConfTarget = 10
434427
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
435428

436429
// We set our mock fee estimate for our target sweep confs to be our
@@ -442,11 +435,6 @@ func TestPreimagePush(t *testing.T) {
442435
),
443436
)
444437

445-
// We set the fee estimate for our default confirmation target very
446-
// low, so that once we drop down to our default confs we will start
447-
// trying to sweep the preimage.
448-
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 1)
449-
450438
cfg := newSwapConfig(
451439
&lnd.LndServices, newStoreMock(t), server,
452440
)
@@ -520,15 +508,15 @@ func TestPreimagePush(t *testing.T) {
520508
// preimage is not revealed, we also do not expect a preimage push.
521509
expiryChan <- testTime
522510

523-
// Now, we notify the height at which the client will start using the
524-
// default confirmation target. This has the effect of lowering our fees
525-
// so that the client still start sweeping.
526-
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta -
527-
DefaultSweepConfTargetDelta
528-
blockEpochChan <- defaultConfTargetHeight
511+
// Now we decrease our fees for the swap's confirmation target to less
512+
// than the maximum miner fee.
513+
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
514+
testReq.MaxMinerFee/2,
515+
))
529516

530-
// This time when we tick the expiry chan, our fees are lower than the
531-
// swap max, so we expect it to prompt a sweep.
517+
// Now when we report a new block and tick our expiry fee timer, and
518+
// fees are acceptably low so we expect our sweep to be published.
519+
blockEpochChan <- ctx.Lnd.Height + 2
532520
expiryChan <- testTime
533521

534522
// Expect a signing request for the HTLC success transaction.
@@ -593,3 +581,119 @@ func TestPreimagePush(t *testing.T) {
593581

594582
require.NoError(t, <-errChan)
595583
}
584+
585+
// TestExpiryBeforeReveal tests the case where the on-chain HTLC expires before
586+
// we have revealed our preimage, demonstrating that we do not reveal our
587+
// preimage once we've reached our expiry height.
588+
func TestExpiryBeforeReveal(t *testing.T) {
589+
defer test.Guard(t)()
590+
591+
lnd := test.NewMockLnd()
592+
ctx := test.NewContext(t, lnd)
593+
server := newServerMock(lnd)
594+
595+
testReq := *testRequest
596+
597+
// Set on-chain HTLC CLTV.
598+
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
599+
600+
// Set our fee estimate to higher than our max miner fee will allow.
601+
lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
602+
testReq.MaxMinerFee*2,
603+
))
604+
605+
// Setup the cfg using mock server and init a loop out request.
606+
cfg := newSwapConfig(
607+
&lnd.LndServices, newStoreMock(t), server,
608+
)
609+
initResult, err := newLoopOutSwap(
610+
context.Background(), cfg, ctx.Lnd.Height, &testReq,
611+
)
612+
require.NoError(t, err)
613+
swap := initResult.swap
614+
615+
// Set up the required dependencies to execute the swap.
616+
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
617+
blockEpochChan := make(chan interface{})
618+
statusChan := make(chan SwapInfo)
619+
expiryChan := make(chan time.Time)
620+
timerFactory := func(_ time.Duration) <-chan time.Time {
621+
return expiryChan
622+
}
623+
624+
errChan := make(chan error)
625+
go func() {
626+
err := swap.execute(context.Background(), &executeConfig{
627+
statusChan: statusChan,
628+
blockEpochChan: blockEpochChan,
629+
timerFactory: timerFactory,
630+
sweeper: sweeper,
631+
}, ctx.Lnd.Height)
632+
if err != nil {
633+
log.Error(err)
634+
}
635+
errChan <- err
636+
}()
637+
638+
// The swap should be found in its initial state.
639+
cfg.store.(*storeMock).assertLoopOutStored()
640+
state := <-statusChan
641+
require.Equal(t, loopdb.StateInitiated, state.State)
642+
643+
// We'll then pay both the swap and prepay invoice, which should trigger
644+
// the server to publish the on-chain HTLC.
645+
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
646+
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
647+
648+
signalSwapPaymentResult(nil)
649+
signalPrepaymentResult(nil)
650+
651+
// Notify the confirmation notification for the HTLC.
652+
ctx.AssertRegisterConf(false, defaultConfirmations)
653+
654+
// Advance the block height to get the HTLC confirmed.
655+
height := ctx.Lnd.Height + 1
656+
blockEpochChan <- height
657+
658+
htlcTx := wire.NewMsgTx(2)
659+
htlcTx.AddTxOut(&wire.TxOut{
660+
Value: int64(swap.AmountRequested),
661+
PkScript: swap.htlc.PkScript,
662+
})
663+
ctx.NotifyConf(htlcTx)
664+
665+
// The client should then register for a spend of the HTLC and attempt
666+
// to sweep it using the custom confirmation target.
667+
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
668+
669+
// Assert that we made a query to track our payment, as required for
670+
// preimage push tracking.
671+
ctx.AssertTrackPayment()
672+
673+
// Tick the expiry channel. Because our max miner fee is too high, we
674+
// won't attempt a sweep at this point.
675+
expiryChan <- testTime
676+
677+
// Now we decrease our conf target to less than our max miner fee.
678+
lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
679+
testReq.MaxMinerFee/2,
680+
))
681+
682+
// Advance the block height to the point where we would do timeout
683+
// instead of pushing the preimage.
684+
blockEpochChan <- testReq.Expiry + height
685+
686+
// Tick our expiry channel again to trigger another sweep attempt.
687+
expiryChan <- testTime
688+
689+
// We should see our swap marked as failed.
690+
cfg.store.(*storeMock).assertLoopOutState(
691+
loopdb.StateFailTimeout,
692+
)
693+
status := <-statusChan
694+
require.Equal(
695+
t, status.State, loopdb.StateFailTimeout,
696+
)
697+
698+
require.Nil(t, <-errChan)
699+
}

test/lnd_services_mock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,5 +258,5 @@ func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice,
258258
func (s *LndMockServices) SetFeeEstimate(confTarget int32,
259259
feeEstimate chainfee.SatPerKWeight) {
260260

261-
s.WalletKit.(*mockWalletKit).feeEstimates[confTarget] = feeEstimate
261+
s.WalletKit.(*mockWalletKit).setFeeEstimate(confTarget, feeEstimate)
262262
}

test/walletkit_mock.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package test
33
import (
44
"context"
55
"errors"
6+
"sync"
67
"time"
78

89
"github.com/btcsuite/btcd/chaincfg"
@@ -21,9 +22,11 @@ import (
2122
var DefaultMockFee = chainfee.SatPerKWeight(10000)
2223

2324
type mockWalletKit struct {
24-
lnd *LndMockServices
25-
keyIndex int32
26-
feeEstimates map[int32]chainfee.SatPerKWeight
25+
lnd *LndMockServices
26+
keyIndex int32
27+
28+
feeEstimateLock sync.Mutex
29+
feeEstimates map[int32]chainfee.SatPerKWeight
2730
}
2831

2932
var _ lndclient.WalletKitClient = (*mockWalletKit)(nil)
@@ -118,9 +121,19 @@ func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut,
118121
return &tx, nil
119122
}
120123

124+
func (m *mockWalletKit) setFeeEstimate(confTarget int32, fee chainfee.SatPerKWeight) {
125+
m.feeEstimateLock.Lock()
126+
defer m.feeEstimateLock.Unlock()
127+
128+
m.feeEstimates[confTarget] = fee
129+
}
130+
121131
func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
122132
chainfee.SatPerKWeight, error) {
123133

134+
m.feeEstimateLock.Lock()
135+
defer m.feeEstimateLock.Unlock()
136+
124137
if confTarget <= 1 {
125138
return 0, errors.New("conf target must be greater than 1")
126139
}

0 commit comments

Comments
 (0)