Skip to content

Commit 9b9f64d

Browse files
authored
Merge pull request #347 from carlaKC/189-loopinfees
loopin: store loop in on-chain fees
2 parents 5a639a2 + d928d53 commit 9b9f64d

File tree

4 files changed

+109
-25
lines changed

4 files changed

+109
-25
lines changed

loopin.go

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/btcsuite/btcutil"
1313

1414
"github.com/btcsuite/btcd/chaincfg/chainhash"
15+
"github.com/btcsuite/btcd/mempool"
1516
"github.com/btcsuite/btcd/wire"
1617
"github.com/lightninglabs/lndclient"
1718
"github.com/lightninglabs/loop/labels"
@@ -21,6 +22,7 @@ import (
2122
"github.com/lightningnetwork/lnd/channeldb"
2223
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
2324
"github.com/lightningnetwork/lnd/lntypes"
25+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
2426
"github.com/lightningnetwork/lnd/lnwire"
2527
)
2628

@@ -311,7 +313,7 @@ func awaitProbe(ctx context.Context, lnd lndclient.LndServices,
311313

312314
// resumeLoopInSwap returns a swap object representing a pending swap that has
313315
// been restored from the database.
314-
func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
316+
func resumeLoopInSwap(_ context.Context, cfg *swapConfig,
315317
pend *loopdb.LoopIn) (*loopInSwap, error) {
316318

317319
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
@@ -339,6 +341,7 @@ func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
339341
swap.state = lastUpdate.State
340342
swap.lastUpdateTime = lastUpdate.Time
341343
swap.htlcTxHash = lastUpdate.HtlcTxHash
344+
swap.cost = lastUpdate.Cost
342345
}
343346

344347
return swap, nil
@@ -516,8 +519,6 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
516519
return s.persistAndAnnounceState(globalCtx)
517520
}
518521

519-
// TODO: Add miner fee of htlc tx to swap cost balance.
520-
521522
// The server is expected to see the htlc on-chain and knowing that it
522523
// can sweep that htlc with the preimage, it should pay our swap
523524
// invoice, receive the preimage and sweep the htlc. We are waiting for
@@ -662,8 +663,11 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
662663
if err != nil {
663664
return false, fmt.Errorf("send outputs: %v", err)
664665
}
666+
665667
txHash := tx.TxHash()
666-
s.log.Infof("Published on chain HTLC tx %v", txHash)
668+
fee := getTxFee(tx, feeRate.FeePerKVByte())
669+
670+
s.log.Infof("Published on chain HTLC tx %v, fee: %v", txHash, fee)
667671

668672
// Persist the htlc hash so that after a restart we are still waiting
669673
// for our own htlc. We don't need to announce to clients, because the
@@ -672,6 +676,12 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
672676
// TODO(joostjager): Store tx hash before calling SendOutputs. This is
673677
// not yet possible with the current lnd api.
674678
s.htlcTxHash = &txHash
679+
680+
// We do not expect any on-chain fees to be recorded yet, and we only
681+
// publish our htlc once, so we set our total on-chain costs to equal
682+
// the fee for publishing the htlc.
683+
s.cost.Onchain = fee
684+
675685
s.lastUpdateTime = time.Now()
676686
if err := s.persistState(); err != nil {
677687
return false, fmt.Errorf("persist htlc tx: %v", err)
@@ -681,6 +691,16 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
681691

682692
}
683693

694+
// getTxFee calculates our fee for a transaction that we have broadcast. We use
695+
// sat per kvbyte because this is what lnd uses, and we will run into rounding
696+
// issues if we do not use the same fee rate as lnd.
697+
func getTxFee(tx *wire.MsgTx, fee chainfee.SatPerKVByte) btcutil.Amount {
698+
btcTx := btcutil.NewTx(tx)
699+
vsize := mempool.GetTxVirtualSize(btcTx)
700+
701+
return fee.FeeForVSize(vsize)
702+
}
703+
684704
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
685705
// the swap invoice is either settled or canceled. If the htlc times out, the
686706
// timeout tx will be published.
@@ -709,17 +729,18 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
709729
}
710730

711731
// checkTimeout publishes the timeout tx if the contract has expired.
712-
checkTimeout := func() error {
732+
checkTimeout := func() (btcutil.Amount, error) {
713733
if s.height >= s.LoopInContract.CltvExpiry {
714734
return s.publishTimeoutTx(ctx, htlcOutpoint, htlcValue)
715735
}
716736

717-
return nil
737+
return 0, nil
718738
}
719739

720740
// Check timeout at current height. After a restart we may want to
721741
// publish the tx immediately.
722-
err = checkTimeout()
742+
var sweepFee btcutil.Amount
743+
sweepFee, err = checkTimeout()
723744
if err != nil {
724745
return err
725746
}
@@ -737,7 +758,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
737758
case notification := <-s.blockEpochChan:
738759
s.height = notification.(int32)
739760

740-
err := checkTimeout()
761+
sweepFee, err = checkTimeout()
741762
if err != nil {
742763
return err
743764
}
@@ -749,7 +770,7 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
749770
spendDetails.SpenderTxHash)
750771

751772
err := s.processHtlcSpend(
752-
ctx, spendDetails, htlcValue,
773+
ctx, spendDetails, htlcValue, sweepFee,
753774
)
754775
if err != nil {
755776
return err
@@ -804,7 +825,8 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
804825
}
805826

806827
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
807-
spend *chainntnfs.SpendDetail, htlcValue btcutil.Amount) error {
828+
spend *chainntnfs.SpendDetail, htlcValue,
829+
sweepFee btcutil.Amount) error {
808830

809831
// Determine the htlc input of the spending tx and inspect the witness
810832
// to findout whether a success or a timeout tx spend the htlc.
@@ -817,6 +839,9 @@ func (s *loopInSwap) processHtlcSpend(ctx context.Context,
817839
// server cost balance.
818840
s.cost.Server += htlcValue
819841
} else {
842+
// We needed another on chain tx to sweep the timeout clause,
843+
// which we now include in our costs.
844+
s.cost.Onchain += sweepFee
820845
s.setState(loopdb.StateFailTimeout)
821846

822847
// Now that the timeout tx confirmed, we can safely cancel the
@@ -828,23 +853,24 @@ func (s *loopInSwap) processHtlcSpend(ctx context.Context,
828853
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
829854
return err
830855
}
831-
832-
// TODO: Add miner fee of timeout tx to swap cost balance.
833856
}
834857

835858
return nil
836859
}
837860

838-
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
839-
// The swap failed and we are reclaiming our funds.
861+
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired,
862+
// returning the fee that is paid by the sweep tx. We cannot update our swap
863+
// costs in this function because it is called multiple times. The swap failed
864+
// and we are reclaiming our funds.
840865
func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
841-
htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) error {
866+
htlcOutpoint *wire.OutPoint, htlcValue btcutil.Amount) (btcutil.Amount,
867+
error) {
842868

843869
if s.timeoutAddr == nil {
844870
var err error
845871
s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(ctx)
846872
if err != nil {
847-
return err
873+
return 0, err
848874
}
849875
}
850876

@@ -854,7 +880,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
854880
TimeoutTxConfTarget,
855881
)
856882
if err != nil {
857-
return err
883+
return 0, err
858884
}
859885

860886
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
@@ -867,7 +893,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
867893
witnessFunc, htlcValue, fee, s.timeoutAddr,
868894
)
869895
if err != nil {
870-
return err
896+
return 0, err
871897
}
872898

873899
timeoutTxHash := timeoutTx.TxHash()
@@ -882,7 +908,7 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
882908
s.log.Warnf("publish timeout: %v", err)
883909
}
884910

885-
return nil
911+
return fee, nil
886912
}
887913

888914
// persistAndAnnounceState updates the swap state on disk and sends out an

loopin_test.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,17 @@ func TestLoopInSuccess(t *testing.T) {
6464
// Expect htlc to be published.
6565
htlcTx := <-ctx.lnd.SendOutputsChannel
6666

67-
// Expect the same state to be written again with the htlc tx hash.
67+
// We expect our cost to use the mock fee rate we set for our conf
68+
// target.
69+
cost := loopdb.SwapCost{
70+
Onchain: getTxFee(&htlcTx, test.DefaultMockFee.FeePerKVByte()),
71+
}
72+
73+
// Expect the same state to be written again with the htlc tx hash
74+
// and on chain fee.
6875
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
6976
require.NotNil(t, state.HtlcTxHash)
77+
require.Equal(t, cost, state.Cost)
7078

7179
// Expect register for htlc conf.
7280
<-ctx.lnd.RegisterConfChannel
@@ -186,14 +194,24 @@ func testLoopInTimeout(t *testing.T,
186194
ctx.assertState(loopdb.StateHtlcPublished)
187195
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
188196

189-
var htlcTx wire.MsgTx
197+
var (
198+
htlcTx wire.MsgTx
199+
cost loopdb.SwapCost
200+
)
190201
if externalValue == 0 {
191202
// Expect htlc to be published.
192203
htlcTx = <-ctx.lnd.SendOutputsChannel
204+
cost = loopdb.SwapCost{
205+
Onchain: getTxFee(
206+
&htlcTx, test.DefaultMockFee.FeePerKVByte(),
207+
),
208+
}
193209

194-
// Expect the same state to be written again with the htlc tx hash.
210+
// Expect the same state to be written again with the htlc tx
211+
// hash and cost.
195212
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
196213
require.NotNil(t, state.HtlcTxHash)
214+
require.Equal(t, cost, state.Cost)
197215
} else {
198216
// Create an external htlc publish tx.
199217
var pkScript []byte
@@ -257,6 +275,15 @@ func testLoopInTimeout(t *testing.T,
257275
// Expect timeout tx to be published.
258276
timeoutTx := <-ctx.lnd.TxPublishChannel
259277

278+
// We can just get our sweep fee as we would in the swap code because
279+
// our estimate is static.
280+
fee, err := s.sweeper.GetSweepFee(
281+
context.Background(), s.htlc.AddTimeoutToEstimator,
282+
s.timeoutAddr, TimeoutTxConfTarget,
283+
)
284+
require.NoError(t, err)
285+
cost.Onchain += fee
286+
260287
// Confirm timeout tx.
261288
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
262289
SpendingTx: timeoutTx,
@@ -273,7 +300,8 @@ func testLoopInTimeout(t *testing.T,
273300
}
274301

275302
ctx.assertState(loopdb.StateFailTimeout)
276-
ctx.store.assertLoopInState(loopdb.StateFailTimeout)
303+
state := ctx.store.assertLoopInState(loopdb.StateFailTimeout)
304+
require.Equal(t, cost, state.Cost)
277305

278306
err = <-errChan
279307
if err != nil {
@@ -360,6 +388,16 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
360388
},
361389
}
362390

391+
// If we have already published the htlc, we expect our cost to already
392+
// be published.
393+
var cost loopdb.SwapCost
394+
if state == loopdb.StateHtlcPublished {
395+
cost = loopdb.SwapCost{
396+
Onchain: 999,
397+
}
398+
pendSwap.Loop.Events[0].Cost = cost
399+
}
400+
363401
htlc, err := swap.NewHtlc(
364402
scriptVersion, contract.CltvExpiry, contract.SenderKey,
365403
contract.ReceiverKey, testPreimage.Hash(), swap.HtlcNP2WSH,
@@ -431,6 +469,11 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
431469

432470
// Expect htlc to be published.
433471
htlcTx = <-ctx.lnd.SendOutputsChannel
472+
cost = loopdb.SwapCost{
473+
Onchain: getTxFee(
474+
&htlcTx, test.DefaultMockFee.FeePerKVByte(),
475+
),
476+
}
434477

435478
// Expect the same state to be written again with the htlc tx
436479
// hash.
@@ -465,10 +508,11 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
465508

466509
// Server has already paid invoice before spending the htlc. Signal
467510
// settled.
468-
subscription.Update <- lndclient.InvoiceUpdate{
511+
invoiceUpdate := lndclient.InvoiceUpdate{
469512
State: channeldb.ContractSettled,
470513
AmtPaid: 49000,
471514
}
515+
subscription.Update <- invoiceUpdate
472516

473517
// Swap is expected to move to the state InvoiceSettled
474518
ctx.assertState(loopdb.StateInvoiceSettled)
@@ -488,4 +532,12 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
488532
}
489533

490534
ctx.assertState(loopdb.StateSuccess)
535+
finalState := ctx.store.assertLoopInState(loopdb.StateSuccess)
536+
537+
// We expect our server fee to reflect as the difference between htlc
538+
// value and invoice amount paid. We use our original on-chain cost, set
539+
// earlier in the test, because we expect this value to be unchanged.
540+
cost.Server = btcutil.Amount(htlcTx.TxOut[0].Value) -
541+
invoiceUpdate.AmtPaid
542+
require.Equal(t, cost, finalState.Cost)
491543
}

release_notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ This file tracks release notes for the loop client.
3434
#### Bug Fixes
3535
* The loop dockerfile has been updated to use the `make` command so that the
3636
latest commit hash of the code being run will be included in `loopd`.
37+
* A bug where loop in on-chain fees were not recorded properly has been
38+
addressed.

test/walletkit_mock.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1717
)
1818

19+
// DefaultMockFee is the default value we use for fee estimates when no values
20+
// are set for specific conf targets.
21+
var DefaultMockFee = chainfee.SatPerKWeight(10000)
22+
1923
type mockWalletKit struct {
2024
lnd *LndMockServices
2125
keyIndex int32
@@ -123,7 +127,7 @@ func (m *mockWalletKit) EstimateFee(ctx context.Context, confTarget int32) (
123127

124128
feeEstimate, ok := m.feeEstimates[confTarget]
125129
if !ok {
126-
return 10000, nil
130+
return DefaultMockFee, nil
127131
}
128132

129133
return feeEstimate, nil

0 commit comments

Comments
 (0)