mirror of
https://github.com/lightninglabs/loop
synced 2024-11-09 19:10:47 +00:00
Merge pull request #372 from carlaKC/sweep-abandon
loopout: do not reveal preimage too close to expiry
This commit is contained in:
commit
245e6b7917
42
loopout.go
42
loopout.go
@ -438,6 +438,12 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If spend details are nil, we resolved the swap without waiting for
|
||||||
|
// its spend, so we can exit.
|
||||||
|
if spendDetails == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Inspect witness stack to see if it is a success transaction. We
|
// Inspect witness stack to see if it is a success transaction. We
|
||||||
// don't just try to match with the hash of our sweep tx, because it
|
// don't just try to match with the hash of our sweep tx, because it
|
||||||
// may be swept by a different (fee) sweep tx from a previous run.
|
// may be swept by a different (fee) sweep tx from a previous run.
|
||||||
@ -854,6 +860,14 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the result of our spend func was that the swap
|
||||||
|
// has reached a final state, then we return nil spend
|
||||||
|
// details, because there is no further action required
|
||||||
|
// for this swap.
|
||||||
|
if s.state.Type() != loopdb.StateTypePending {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// If our off chain payment is not yet complete, we
|
// If our off chain payment is not yet complete, we
|
||||||
// try to push our preimage to the server.
|
// try to push our preimage to the server.
|
||||||
if !paymentComplete {
|
if !paymentComplete {
|
||||||
@ -889,7 +903,9 @@ func (s *loopOutSwap) pushPreimage(ctx context.Context) {
|
|||||||
|
|
||||||
// sweep tries to sweep the given htlc to a destination address. It takes into
|
// sweep tries to sweep the given htlc to a destination address. It takes into
|
||||||
// account the max miner fee and marks the preimage as revealed when it
|
// account the max miner fee and marks the preimage as revealed when it
|
||||||
// published the tx.
|
// published the tx. If the preimage has not yet been revealed, and the time
|
||||||
|
// during which we can safely reveal it has passed, the swap will be marked
|
||||||
|
// as failed, and the function will return.
|
||||||
//
|
//
|
||||||
// TODO: Use lnd sweeper?
|
// TODO: Use lnd sweeper?
|
||||||
func (s *loopOutSwap) sweep(ctx context.Context,
|
func (s *loopOutSwap) sweep(ctx context.Context,
|
||||||
@ -900,16 +916,36 @@ func (s *loopOutSwap) sweep(ctx context.Context,
|
|||||||
return s.htlc.GenSuccessWitness(sig, s.Preimage)
|
return s.htlc.GenSuccessWitness(sig, s.Preimage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remainingBlocks := s.CltvExpiry - s.height
|
||||||
|
blocksToLastReveal := remainingBlocks - MinLoopOutPreimageRevealDelta
|
||||||
|
preimageRevealed := s.state == loopdb.StatePreimageRevealed
|
||||||
|
|
||||||
|
// If we have not revealed our preimage, and we don't have time left
|
||||||
|
// to sweep the swap, we abandon the swap because we can no longer
|
||||||
|
// sweep the success path (without potentially having to compete with
|
||||||
|
// the server's timeout sweep), and we have not had any coins pulled
|
||||||
|
// off-chain.
|
||||||
|
if blocksToLastReveal <= 0 && !preimageRevealed {
|
||||||
|
s.log.Infof("Preimage can no longer be safely revealed: "+
|
||||||
|
"expires at: %v, current height: %v", s.CltvExpiry,
|
||||||
|
s.height)
|
||||||
|
|
||||||
|
s.state = loopdb.StateFailTimeout
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the transaction fee based on the confirmation target
|
// Calculate the transaction fee based on the confirmation target
|
||||||
// required to sweep the HTLC before the timeout. We'll use the
|
// required to sweep the HTLC before the timeout. We'll use the
|
||||||
// confirmation target provided by the client unless we've come too
|
// confirmation target provided by the client unless we've come too
|
||||||
// close to the expiration height, in which case we'll use the default
|
// close to the expiration height, in which case we'll use the default
|
||||||
// if it is better than what the client provided.
|
// if it is better than what the client provided.
|
||||||
confTarget := s.SweepConfTarget
|
confTarget := s.SweepConfTarget
|
||||||
if s.CltvExpiry-s.height <= DefaultSweepConfTargetDelta &&
|
if remainingBlocks <= DefaultSweepConfTargetDelta &&
|
||||||
confTarget > DefaultSweepConfTarget {
|
confTarget > DefaultSweepConfTarget {
|
||||||
|
|
||||||
confTarget = DefaultSweepConfTarget
|
confTarget = DefaultSweepConfTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
fee, err := s.sweeper.GetSweepFee(
|
fee, err := s.sweeper.GetSweepFee(
|
||||||
ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget,
|
ctx, s.htlc.AddSuccessToEstimator, s.DestAddr, confTarget,
|
||||||
)
|
)
|
||||||
@ -922,7 +958,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
|
|||||||
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
|
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
|
||||||
fee, s.MaxMinerFee)
|
fee, s.MaxMinerFee)
|
||||||
|
|
||||||
if s.state == loopdb.StatePreimageRevealed {
|
if preimageRevealed {
|
||||||
// The currently required fee exceeds the max, but we
|
// The currently required fee exceeds the max, but we
|
||||||
// already revealed the preimage. The best we can do now
|
// already revealed the preimage. The best we can do now
|
||||||
// is to republish with the max fee.
|
// is to republish with the max fee.
|
||||||
|
148
loopout_test.go
148
loopout_test.go
@ -414,11 +414,7 @@ func TestCustomSweepConfTarget(t *testing.T) {
|
|||||||
// not detected our settle) and settle the off chain htlc, indicating that the
|
// not detected our settle) and settle the off chain htlc, indicating that the
|
||||||
// server successfully settled using the preimage push. In this test, we need
|
// server successfully settled using the preimage push. In this test, we need
|
||||||
// to start with a fee rate that will be too high, then progress to an
|
// to start with a fee rate that will be too high, then progress to an
|
||||||
// acceptable one. We do this by starting with a high confirmation target with
|
// acceptable one.
|
||||||
// a high fee, and setting the default confirmation fee (which our swap will
|
|
||||||
// drop down to if it is not confirming in time) to a lower fee. This is not
|
|
||||||
// intuitive (lower confs having lower fees), but it allows up to mock fee
|
|
||||||
// changes.
|
|
||||||
func TestPreimagePush(t *testing.T) {
|
func TestPreimagePush(t *testing.T) {
|
||||||
defer test.Guard(t)()
|
defer test.Guard(t)()
|
||||||
|
|
||||||
@ -426,11 +422,8 @@ func TestPreimagePush(t *testing.T) {
|
|||||||
ctx := test.NewContext(t, lnd)
|
ctx := test.NewContext(t, lnd)
|
||||||
server := newServerMock(lnd)
|
server := newServerMock(lnd)
|
||||||
|
|
||||||
// Start with a high confirmation delta which will have a very high fee
|
|
||||||
// attached to it.
|
|
||||||
testReq := *testRequest
|
testReq := *testRequest
|
||||||
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
|
testReq.SweepConfTarget = 10
|
||||||
DefaultSweepConfTargetDelta - 1
|
|
||||||
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
|
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
|
||||||
|
|
||||||
// We set our mock fee estimate for our target sweep confs to be our
|
// We set our mock fee estimate for our target sweep confs to be our
|
||||||
@ -442,11 +435,6 @@ func TestPreimagePush(t *testing.T) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// We set the fee estimate for our default confirmation target very
|
|
||||||
// low, so that once we drop down to our default confs we will start
|
|
||||||
// trying to sweep the preimage.
|
|
||||||
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 1)
|
|
||||||
|
|
||||||
cfg := newSwapConfig(
|
cfg := newSwapConfig(
|
||||||
&lnd.LndServices, newStoreMock(t), server,
|
&lnd.LndServices, newStoreMock(t), server,
|
||||||
)
|
)
|
||||||
@ -520,15 +508,15 @@ func TestPreimagePush(t *testing.T) {
|
|||||||
// preimage is not revealed, we also do not expect a preimage push.
|
// preimage is not revealed, we also do not expect a preimage push.
|
||||||
expiryChan <- testTime
|
expiryChan <- testTime
|
||||||
|
|
||||||
// Now, we notify the height at which the client will start using the
|
// Now we decrease our fees for the swap's confirmation target to less
|
||||||
// default confirmation target. This has the effect of lowering our fees
|
// than the maximum miner fee.
|
||||||
// so that the client still start sweeping.
|
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
|
||||||
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta -
|
testReq.MaxMinerFee/2,
|
||||||
DefaultSweepConfTargetDelta
|
))
|
||||||
blockEpochChan <- defaultConfTargetHeight
|
|
||||||
|
|
||||||
// This time when we tick the expiry chan, our fees are lower than the
|
// Now when we report a new block and tick our expiry fee timer, and
|
||||||
// swap max, so we expect it to prompt a sweep.
|
// fees are acceptably low so we expect our sweep to be published.
|
||||||
|
blockEpochChan <- ctx.Lnd.Height + 2
|
||||||
expiryChan <- testTime
|
expiryChan <- testTime
|
||||||
|
|
||||||
// Expect a signing request for the HTLC success transaction.
|
// Expect a signing request for the HTLC success transaction.
|
||||||
@ -593,3 +581,119 @@ func TestPreimagePush(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, <-errChan)
|
require.NoError(t, <-errChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestExpiryBeforeReveal tests the case where the on-chain HTLC expires before
|
||||||
|
// we have revealed our preimage, demonstrating that we do not reveal our
|
||||||
|
// preimage once we've reached our expiry height.
|
||||||
|
func TestExpiryBeforeReveal(t *testing.T) {
|
||||||
|
defer test.Guard(t)()
|
||||||
|
|
||||||
|
lnd := test.NewMockLnd()
|
||||||
|
ctx := test.NewContext(t, lnd)
|
||||||
|
server := newServerMock(lnd)
|
||||||
|
|
||||||
|
testReq := *testRequest
|
||||||
|
|
||||||
|
// Set on-chain HTLC CLTV.
|
||||||
|
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
|
||||||
|
|
||||||
|
// Set our fee estimate to higher than our max miner fee will allow.
|
||||||
|
lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
|
||||||
|
testReq.MaxMinerFee*2,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Setup the cfg using mock server and init a loop out request.
|
||||||
|
cfg := newSwapConfig(
|
||||||
|
&lnd.LndServices, newStoreMock(t), server,
|
||||||
|
)
|
||||||
|
initResult, err := newLoopOutSwap(
|
||||||
|
context.Background(), cfg, ctx.Lnd.Height, &testReq,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
swap := initResult.swap
|
||||||
|
|
||||||
|
// Set up the required dependencies to execute the swap.
|
||||||
|
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
|
||||||
|
blockEpochChan := make(chan interface{})
|
||||||
|
statusChan := make(chan SwapInfo)
|
||||||
|
expiryChan := make(chan time.Time)
|
||||||
|
timerFactory := func(_ time.Duration) <-chan time.Time {
|
||||||
|
return expiryChan
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error)
|
||||||
|
go func() {
|
||||||
|
err := swap.execute(context.Background(), &executeConfig{
|
||||||
|
statusChan: statusChan,
|
||||||
|
blockEpochChan: blockEpochChan,
|
||||||
|
timerFactory: timerFactory,
|
||||||
|
sweeper: sweeper,
|
||||||
|
}, ctx.Lnd.Height)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
// The swap should be found in its initial state.
|
||||||
|
cfg.store.(*storeMock).assertLoopOutStored()
|
||||||
|
state := <-statusChan
|
||||||
|
require.Equal(t, loopdb.StateInitiated, state.State)
|
||||||
|
|
||||||
|
// We'll then pay both the swap and prepay invoice, which should trigger
|
||||||
|
// the server to publish the on-chain HTLC.
|
||||||
|
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
|
||||||
|
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
|
||||||
|
|
||||||
|
signalSwapPaymentResult(nil)
|
||||||
|
signalPrepaymentResult(nil)
|
||||||
|
|
||||||
|
// Notify the confirmation notification for the HTLC.
|
||||||
|
ctx.AssertRegisterConf(false, defaultConfirmations)
|
||||||
|
|
||||||
|
// Advance the block height to get the HTLC confirmed.
|
||||||
|
height := ctx.Lnd.Height + 1
|
||||||
|
blockEpochChan <- height
|
||||||
|
|
||||||
|
htlcTx := wire.NewMsgTx(2)
|
||||||
|
htlcTx.AddTxOut(&wire.TxOut{
|
||||||
|
Value: int64(swap.AmountRequested),
|
||||||
|
PkScript: swap.htlc.PkScript,
|
||||||
|
})
|
||||||
|
ctx.NotifyConf(htlcTx)
|
||||||
|
|
||||||
|
// The client should then register for a spend of the HTLC and attempt
|
||||||
|
// to sweep it using the custom confirmation target.
|
||||||
|
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
|
||||||
|
|
||||||
|
// Assert that we made a query to track our payment, as required for
|
||||||
|
// preimage push tracking.
|
||||||
|
ctx.AssertTrackPayment()
|
||||||
|
|
||||||
|
// Tick the expiry channel. Because our max miner fee is too high, we
|
||||||
|
// won't attempt a sweep at this point.
|
||||||
|
expiryChan <- testTime
|
||||||
|
|
||||||
|
// Now we decrease our conf target to less than our max miner fee.
|
||||||
|
lnd.SetFeeEstimate(testReq.SweepConfTarget, chainfee.SatPerKWeight(
|
||||||
|
testReq.MaxMinerFee/2,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Advance the block height to the point where we would do timeout
|
||||||
|
// instead of pushing the preimage.
|
||||||
|
blockEpochChan <- testReq.Expiry + height
|
||||||
|
|
||||||
|
// Tick our expiry channel again to trigger another sweep attempt.
|
||||||
|
expiryChan <- testTime
|
||||||
|
|
||||||
|
// We should see our swap marked as failed.
|
||||||
|
cfg.store.(*storeMock).assertLoopOutState(
|
||||||
|
loopdb.StateFailTimeout,
|
||||||
|
)
|
||||||
|
status := <-statusChan
|
||||||
|
require.Equal(
|
||||||
|
t, status.State, loopdb.StateFailTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Nil(t, <-errChan)
|
||||||
|
}
|
||||||
|
@ -258,5 +258,5 @@ func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice,
|
|||||||
func (s *LndMockServices) SetFeeEstimate(confTarget int32,
|
func (s *LndMockServices) SetFeeEstimate(confTarget int32,
|
||||||
feeEstimate chainfee.SatPerKWeight) {
|
feeEstimate chainfee.SatPerKWeight) {
|
||||||
|
|
||||||
s.WalletKit.(*mockWalletKit).feeEstimates[confTarget] = feeEstimate
|
s.WalletKit.(*mockWalletKit).setFeeEstimate(confTarget, feeEstimate)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
@ -23,6 +24,8 @@ var DefaultMockFee = chainfee.SatPerKWeight(10000)
|
|||||||
type mockWalletKit struct {
|
type mockWalletKit struct {
|
||||||
lnd *LndMockServices
|
lnd *LndMockServices
|
||||||
keyIndex int32
|
keyIndex int32
|
||||||
|
|
||||||
|
feeEstimateLock sync.Mutex
|
||||||
feeEstimates map[int32]chainfee.SatPerKWeight
|
feeEstimates map[int32]chainfee.SatPerKWeight
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,9 +121,19 @@ func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut,
|
|||||||
return &tx, nil
|
return &tx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockWalletKit) setFeeEstimate(confTarget int32, fee chainfee.SatPerKWeight) {
|
||||||
|
m.feeEstimateLock.Lock()
|
||||||
|
defer m.feeEstimateLock.Unlock()
|
||||||
|
|
||||||
|
m.feeEstimates[confTarget] = fee
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
|
func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
|
||||||
chainfee.SatPerKWeight, error) {
|
chainfee.SatPerKWeight, error) {
|
||||||
|
|
||||||
|
m.feeEstimateLock.Lock()
|
||||||
|
defer m.feeEstimateLock.Unlock()
|
||||||
|
|
||||||
if confTarget <= 1 {
|
if confTarget <= 1 {
|
||||||
return 0, errors.New("conf target must be greater than 1")
|
return 0, errors.New("conf target must be greater than 1")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user