Skip to content

Commit b61807b

Browse files
authored
Merge pull request #89 from wpaulino/loop-out-conf-target
loopout: compare delta from htlc expiry correctly
2 parents 0e09677 + e0d23cb commit b61807b

File tree

5 files changed

+205
-12
lines changed

5 files changed

+205
-12
lines changed

loopout.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
597597
// close to the expiration height, in which case we'll use the default
598598
// if it is better than what the client provided.
599599
confTarget := s.SweepConfTarget
600-
if s.CltvExpiry-s.height >= DefaultSweepConfTargetDelta &&
600+
if s.CltvExpiry-s.height <= DefaultSweepConfTargetDelta &&
601601
confTarget > DefaultSweepConfTarget {
602602
confTarget = DefaultSweepConfTarget
603603
}

loopout_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/btcsuite/btcd/blockchain"
10+
"github.com/btcsuite/btcd/wire"
11+
"github.com/btcsuite/btcutil"
912
"github.com/lightninglabs/loop/lndclient"
1013
"github.com/lightninglabs/loop/loopdb"
1114
"github.com/lightninglabs/loop/sweep"
@@ -99,3 +102,166 @@ func TestLateHtlcPublish(t *testing.T) {
99102
t.Fatal(err)
100103
}
101104
}
105+
106+
// TestCustomSweepConfTarget ensures we are able to sweep a Loop Out HTLC with a
107+
// custom confirmation target.
108+
func TestCustomSweepConfTarget(t *testing.T) {
109+
defer test.Guard(t)()
110+
111+
lnd := test.NewMockLnd()
112+
ctx := test.NewContext(t, lnd)
113+
114+
// Use the highest sweep confirmation target before we attempt to use
115+
// the default.
116+
testRequest.SweepConfTarget = testLoopOutOnChainCltvDelta -
117+
DefaultSweepConfTargetDelta - 1
118+
119+
// Set up custom fee estimates such that the lower confirmation target
120+
// yields a much higher fee rate.
121+
ctx.Lnd.SetFeeEstimate(testRequest.SweepConfTarget, 250)
122+
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
123+
124+
cfg := &swapConfig{
125+
lnd: &lnd.LndServices,
126+
store: newStoreMock(t),
127+
server: newServerMock(),
128+
}
129+
swap, err := newLoopOutSwap(
130+
context.Background(), cfg, ctx.Lnd.Height, testRequest,
131+
)
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
136+
// Set up the required dependencies to execute the swap.
137+
//
138+
// TODO: create test context similar to loopInTestContext.
139+
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
140+
blockEpochChan := make(chan interface{})
141+
statusChan := make(chan SwapInfo)
142+
expiryChan := make(chan time.Time)
143+
timerFactory := func(expiry time.Duration) <-chan time.Time {
144+
return expiryChan
145+
}
146+
147+
errChan := make(chan error)
148+
go func() {
149+
err := swap.execute(context.Background(), &executeConfig{
150+
statusChan: statusChan,
151+
blockEpochChan: blockEpochChan,
152+
timerFactory: timerFactory,
153+
sweeper: sweeper,
154+
}, ctx.Lnd.Height)
155+
if err != nil {
156+
logger.Error(err)
157+
}
158+
errChan <- err
159+
}()
160+
161+
// The swap should be found in its initial state.
162+
cfg.store.(*storeMock).assertLoopOutStored()
163+
state := <-statusChan
164+
if state.State != loopdb.StateInitiated {
165+
t.Fatal("unexpected state")
166+
}
167+
168+
// We'll then pay both the swap and prepay invoice, which should trigger
169+
// the server to publish the on-chain HTLC.
170+
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
171+
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
172+
173+
signalSwapPaymentResult(nil)
174+
signalPrepaymentResult(nil)
175+
176+
// Notify the confirmation notification for the HTLC.
177+
ctx.AssertRegisterConf()
178+
179+
blockEpochChan <- int32(ctx.Lnd.Height + 1)
180+
181+
htlcTx := wire.NewMsgTx(2)
182+
htlcTx.AddTxOut(&wire.TxOut{
183+
Value: int64(swap.AmountRequested),
184+
PkScript: swap.htlc.PkScript,
185+
})
186+
187+
ctx.NotifyConf(htlcTx)
188+
189+
// The client should then register for a spend of the HTLC and attempt
190+
// to sweep it using the custom confirmation target.
191+
ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript)
192+
193+
expiryChan <- time.Now()
194+
195+
cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed)
196+
status := <-statusChan
197+
if status.State != loopdb.StatePreimageRevealed {
198+
t.Fatalf("expected state %v, got %v",
199+
loopdb.StatePreimageRevealed, status.State)
200+
}
201+
202+
// assertSweepTx performs some sanity checks on a sweep transaction to
203+
// ensure it was constructed correctly.
204+
assertSweepTx := func(expConfTarget int32) *wire.MsgTx {
205+
t.Helper()
206+
207+
sweepTx := ctx.ReceiveTx()
208+
if sweepTx.TxIn[0].PreviousOutPoint.Hash != htlcTx.TxHash() {
209+
t.Fatalf("expected sweep tx to spend %v, got %v",
210+
htlcTx.TxHash(), sweepTx.TxIn[0].PreviousOutPoint)
211+
}
212+
213+
// The fee used for the sweep transaction is an estimate based
214+
// on the maximum witness size, so we should expect to see a
215+
// lower fee when using the actual witness size of the
216+
// transaction.
217+
fee := btcutil.Amount(
218+
htlcTx.TxOut[0].Value - sweepTx.TxOut[0].Value,
219+
)
220+
221+
weight := blockchain.GetTransactionWeight(btcutil.NewTx(sweepTx))
222+
feeRate, err := ctx.Lnd.WalletKit.EstimateFee(
223+
context.Background(), expConfTarget,
224+
)
225+
if err != nil {
226+
t.Fatalf("unable to retrieve fee estimate: %v", err)
227+
}
228+
minFee := feeRate.FeeForWeight(weight)
229+
maxFee := btcutil.Amount(float64(minFee) * 1.1)
230+
231+
if fee < minFee && fee > maxFee {
232+
t.Fatalf("expected sweep tx to have fee between %v-%v, "+
233+
"got %v", minFee, maxFee, fee)
234+
}
235+
236+
return sweepTx
237+
}
238+
239+
// The sweep should have a fee that corresponds to the custom
240+
// confirmation target.
241+
sweepTx := assertSweepTx(testRequest.SweepConfTarget)
242+
243+
// We'll then notify the height at which we begin using the default
244+
// confirmation target.
245+
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta -
246+
DefaultSweepConfTargetDelta
247+
blockEpochChan <- int32(defaultConfTargetHeight)
248+
expiryChan <- time.Now()
249+
250+
// We should expect to see another sweep using the higher fee since the
251+
// spend hasn't been confirmed yet.
252+
sweepTx = assertSweepTx(DefaultSweepConfTarget)
253+
254+
// Notify the spend so that the swap reaches its final state.
255+
ctx.NotifySpend(sweepTx, 0)
256+
257+
cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess)
258+
status = <-statusChan
259+
if status.State != loopdb.StateSuccess {
260+
t.Fatalf("expected state %v, got %v", loopdb.StateSuccess,
261+
status.State)
262+
}
263+
264+
if err := <-errChan; err != nil {
265+
t.Fatal(err)
266+
}
267+
}

store_mock_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ func (s *storeMock) UpdateLoopIn(hash lntypes.Hash, time time.Time,
170170
}
171171

172172
updates = append(updates, state)
173-
s.loopOutUpdates[hash] = updates
174-
s.loopOutUpdateChan <- state
173+
s.loopInUpdates[hash] = updates
174+
s.loopInUpdateChan <- state
175175

176176
return nil
177177
}
@@ -205,6 +205,15 @@ func (s *storeMock) assertLoopOutStored() {
205205
}
206206
}
207207

208+
func (s *storeMock) assertLoopOutState(expectedState loopdb.SwapState) {
209+
s.t.Helper()
210+
211+
state := <-s.loopOutUpdateChan
212+
if state.State != expectedState {
213+
s.t.Fatalf("expected state %v, got %v", expectedState, state)
214+
}
215+
}
216+
208217
func (s *storeMock) assertLoopInStored() {
209218
s.t.Helper()
210219

@@ -214,9 +223,9 @@ func (s *storeMock) assertLoopInStored() {
214223
func (s *storeMock) assertLoopInState(expectedState loopdb.SwapState) {
215224
s.t.Helper()
216225

217-
state := <-s.loopOutUpdateChan
226+
state := <-s.loopInUpdateChan
218227
if state.State != expectedState {
219-
s.t.Fatalf("unexpected state")
228+
s.t.Fatalf("expected state %v, got %v", expectedState, state)
220229
}
221230
}
222231

test/lnd_services_mock.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"time"
77

88
"github.com/btcsuite/btcd/chaincfg"
9-
"github.com/lightningnetwork/lnd/lntypes"
10-
"github.com/lightningnetwork/lnd/zpay32"
11-
129
"github.com/btcsuite/btcd/wire"
1310
"github.com/lightninglabs/loop/lndclient"
1411
"github.com/lightningnetwork/lnd/chainntnfs"
12+
"github.com/lightningnetwork/lnd/lntypes"
13+
"github.com/lightningnetwork/lnd/lnwallet"
14+
"github.com/lightningnetwork/lnd/zpay32"
1515
)
1616

1717
var testStartingHeight = int32(600)
@@ -20,7 +20,9 @@ var testStartingHeight = int32(600)
2020
// tests.
2121
func NewMockLnd() *LndMockServices {
2222
lightningClient := &mockLightningClient{}
23-
walletKit := &mockWalletKit{}
23+
walletKit := &mockWalletKit{
24+
feeEstimates: make(map[int32]lnwallet.SatPerKWeight),
25+
}
2426
chainNotifier := &mockChainNotifier{}
2527
signer := &mockSigner{}
2628
invoices := &mockInvoices{}
@@ -197,3 +199,9 @@ func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice,
197199

198200
return zpay32.Decode(request, s.ChainParams)
199201
}
202+
203+
func (s *LndMockServices) SetFeeEstimate(confTarget int32,
204+
feeEstimate lnwallet.SatPerKWeight) {
205+
206+
s.WalletKit.(*mockWalletKit).feeEstimates[confTarget] = feeEstimate
207+
}

test/walletkit_mock.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@ import (
88
"github.com/btcsuite/btcd/chaincfg/chainhash"
99
"github.com/btcsuite/btcd/wire"
1010
"github.com/btcsuite/btcutil"
11+
"github.com/lightninglabs/loop/lndclient"
1112
"github.com/lightningnetwork/lnd/keychain"
1213
"github.com/lightningnetwork/lnd/lnwallet"
1314
)
1415

1516
type mockWalletKit struct {
16-
lnd *LndMockServices
17-
keyIndex int32
17+
lnd *LndMockServices
18+
keyIndex int32
19+
feeEstimates map[int32]lnwallet.SatPerKWeight
1820
}
1921

22+
var _ lndclient.WalletKitClient = (*mockWalletKit)(nil)
23+
2024
func (m *mockWalletKit) DeriveNextKey(ctx context.Context, family int32) (
2125
*keychain.KeyDescriptor, error) {
2226

@@ -87,9 +91,15 @@ func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut,
8791

8892
func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
8993
lnwallet.SatPerKWeight, error) {
94+
9095
if confTarget <= 1 {
9196
return 0, errors.New("conf target must be greater than 1")
9297
}
9398

94-
return 10000, nil
99+
feeEstimate, ok := m.feeEstimates[confTarget]
100+
if !ok {
101+
return 10000, nil
102+
}
103+
104+
return feeEstimate, nil
95105
}

0 commit comments

Comments
 (0)