From fd971d4951912a4c82683a28b1b5695ca130e272 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Fri, 21 Jun 2024 23:20:52 -0300 Subject: [PATCH] sweepbatcher: add field SweepInfo.MinFeeRate MinFeeRate is minimum fee rate that must be used by a batch of the sweep. If it is specified, confTarget is ignored. This is useful for external source of fees. --- sweepbatcher/sweep_batch.go | 14 ++++- sweepbatcher/sweep_batcher.go | 16 +++++- sweepbatcher/sweep_batcher_test.go | 83 ++++++++++++++++++++++-------- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/sweepbatcher/sweep_batch.go b/sweepbatcher/sweep_batch.go index 5a6933f..eed0afd 100644 --- a/sweepbatcher/sweep_batch.go +++ b/sweepbatcher/sweep_batch.go @@ -97,6 +97,10 @@ type sweep struct { // notifier is a collection of channels used to communicate the status // of the sweep back to the swap that requested it. notifier *SpendNotifier + + // minFeeRate is minimum fee rate that must be used by a batch of + // the sweep. If it is specified, confTarget is ignored. + minFeeRate chainfee.SatPerKWeight } // batchState is the state of the batch. @@ -399,9 +403,10 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) { b.sweeps[sweep.swapHash] = *sweep // If this is the primary sweep, we also need to update the - // batch's confirmation target. + // batch's confirmation target and fee rate. if b.primarySweepID == sweep.swapHash { b.cfg.batchConfTarget = sweep.confTarget + b.rbfCache.FeeRate = sweep.minFeeRate } return true, nil @@ -443,6 +448,7 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) { if b.primarySweepID == lntypes.ZeroHash { b.primarySweepID = sweep.swapHash b.cfg.batchConfTarget = sweep.confTarget + b.rbfCache.FeeRate = sweep.minFeeRate // We also need to start the spend monitor for this new primary // sweep. @@ -456,6 +462,12 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) { b.log.Infof("adding sweep %x", sweep.swapHash[:6]) b.sweeps[sweep.swapHash] = *sweep + // Update FeeRate. Max(sweep.minFeeRate) for all the sweeps of + // the batch is the basis for fee bumps. + if b.rbfCache.FeeRate < sweep.minFeeRate { + b.rbfCache.FeeRate = sweep.minFeeRate + } + return true, b.persistSweep(ctx, *sweep, false) } diff --git a/sweepbatcher/sweep_batcher.go b/sweepbatcher/sweep_batcher.go index a523ef9..799961a 100644 --- a/sweepbatcher/sweep_batcher.go +++ b/sweepbatcher/sweep_batcher.go @@ -120,6 +120,10 @@ type SweepInfo struct { // DestAddr is the destination address of the sweep. DestAddr btcutil.Address + + // MinFeeRate is minimum fee rate that must be used by a batch of + // the sweep. If it is specified, confTarget is ignored. + MinFeeRate chainfee.SatPerKWeight } // SweepFetcher is used to get details of a sweep. @@ -523,6 +527,9 @@ func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error { sweeps := make(map[lntypes.Hash]sweep) + // Collect feeRate from sweeps and stored batch. + feeRate := batch.rbfCache.FeeRate + for _, dbSweep := range dbSweeps { sweep, err := b.convertSweep(ctx, dbSweep) if err != nil { @@ -530,11 +537,16 @@ func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error { } sweeps[sweep.swapHash] = *sweep + + // Set minFeeRate to max(sweep.minFeeRate) for all sweeps. + if feeRate < sweep.minFeeRate { + feeRate = sweep.minFeeRate + } } rbfCache := rbfCache{ LastHeight: batch.rbfCache.LastHeight, - FeeRate: batch.rbfCache.FeeRate, + FeeRate: feeRate, } logger := batchPrefixLogger(fmt.Sprintf("%d", batch.id)) @@ -758,6 +770,7 @@ func (b *Batcher) convertSweep(ctx context.Context, dbSweep *dbSweep) ( protocolVersion: s.ProtocolVersion, isExternalAddr: s.IsExternalAddr, destAddr: s.DestAddr, + minFeeRate: s.MinFeeRate, }, nil } @@ -855,5 +868,6 @@ func (b *Batcher) fetchSweep(ctx context.Context, protocolVersion: s.ProtocolVersion, isExternalAddr: s.IsExternalAddr, destAddr: s.DestAddr, + minFeeRate: s.MinFeeRate, }, nil } diff --git a/sweepbatcher/sweep_batcher_test.go b/sweepbatcher/sweep_batcher_test.go index 0c18481..dadfc6a 100644 --- a/sweepbatcher/sweep_batcher_test.go +++ b/sweepbatcher/sweep_batcher_test.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/require" ) @@ -1746,20 +1747,64 @@ func testSweepFetcher(t *testing.T, store testStore, ) require.NoError(t, err) + swapHash := lntypes.Hash{1, 1, 1} + + // Provide min fee rate for the sweep. + feeRate := chainfee.SatPerKWeight(30000) + amt := btcutil.Amount(1_000_000) + weight := lntypes.WeightUnit(445) // Weight for 1-to-1 tx. + bumpedFee := feeRate + 100 + expectedFee := bumpedFee.FeeForWeight(weight) + + swap := &loopdb.LoopOutContract{ + SwapContract: loopdb.SwapContract{ + CltvExpiry: 222, + AmountRequested: amt, + ProtocolVersion: loopdb.ProtocolVersionMuSig2, + HtlcKeys: loopdb.HtlcKeys{ + SenderScriptKey: senderKey, + ReceiverScriptKey: receiverKey, + SenderInternalPubKey: senderKey, + ReceiverInternalPubKey: receiverKey, + }, + }, + DestAddr: destAddr, + SwapInvoice: swapInvoice, + SweepConfTarget: 321, + } + + htlc, err := utils.GetHtlc( + swapHash, &swap.SwapContract, lnd.ChainParams, + ) + require.NoError(t, err) + + sweepInfo := &SweepInfo{ + ConfTarget: 123, + Timeout: 111, + SwapInvoicePaymentAddr: *swapPaymentAddr, + MinFeeRate: feeRate, + ProtocolVersion: loopdb.ProtocolVersionMuSig2, + HTLCKeys: loopdb.HtlcKeys{ + SenderScriptKey: senderKey, + ReceiverScriptKey: receiverKey, + SenderInternalPubKey: senderKey, + ReceiverInternalPubKey: receiverKey, + }, + HTLC: *htlc, + HTLCSuccessEstimator: htlc.AddSuccessToEstimator, + DestAddr: destAddr, + } + sweepFetcher := &sweepFetcherMock{ store: map[lntypes.Hash]*SweepInfo{ - {1, 1, 1}: { - ConfTarget: 123, - Timeout: 111, - SwapInvoicePaymentAddr: *swapPaymentAddr, - }, + swapHash: sweepInfo, }, } // Create a sweep request. sweepReq := SweepRequest{ - SwapHash: lntypes.Hash{1, 1, 1}, - Value: 111, + SwapHash: swapHash, + Value: amt, Outpoint: wire.OutPoint{ Hash: chainhash.Hash{1, 1}, Index: 1, @@ -1770,23 +1815,13 @@ func testSweepFetcher(t *testing.T, store testStore, // Create a swap in the DB. It is needed to satisfy SQL constraints in // case of SQL test. The data is not actually used, since we pass sweep // fetcher, so put different conf target to make sure it is not used. - swap := &loopdb.LoopOutContract{ - SwapContract: loopdb.SwapContract{ - CltvExpiry: 222, - AmountRequested: 222, - }, - - DestAddr: destAddr, - SwapInvoice: swapInvoice, - SweepConfTarget: 321, - } - err = store.CreateLoopOut(ctx, sweepReq.SwapHash, swap) + err = store.CreateLoopOut(ctx, swapHash, swap) require.NoError(t, err) store.AssertLoopOutStored() batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, - testMuSig2SignSweep, nil, lnd.ChainParams, batcherStore, - sweepFetcher) + testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams, + batcherStore, sweepFetcher) var wg sync.WaitGroup wg.Add(1) @@ -1811,7 +1846,7 @@ func testSweepFetcher(t *testing.T, store testStore, // batch. require.Eventually(t, func() bool { // Make sure that the sweep was stored - if !batcherStore.AssertSweepStored(sweepReq.SwapHash) { + if !batcherStore.AssertSweepStored(swapHash) { return false } @@ -1832,6 +1867,12 @@ func testSweepFetcher(t *testing.T, store testStore, return batch.cfg.batchConfTarget == 123 }, test.Timeout, eventuallyCheckFrequency) + // Get the published transaction and check the fee rate. + tx := <-lnd.TxPublishChannel + out := btcutil.Amount(tx.TxOut[0].Value) + gotFee := amt - out + require.Equal(t, expectedFee, gotFee, "fees don't match") + // Make sure we have stored the batch. batches, err := batcherStore.FetchUnconfirmedSweepBatches(ctx) require.NoError(t, err)