Skip to content

Commit 1b9e306

Browse files
committed
sweepbatcher: add option WithNoBumping
It disables fee bumping. It is useful when fee rate is set externally. Add test for fee bumping, testing both modes.
1 parent bff1fd8 commit 1b9e306

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

sweepbatcher/sweep_batch.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ type batchConfig struct {
127127
// batchPublishDelay is the delay between receiving a new block and
128128
// publishing the batch transaction.
129129
batchPublishDelay time.Duration
130+
131+
// noBumping instructs sweepbatcher not to fee bump itself and rely on
132+
// external source of fee rates (MinFeeRate). To change the fee rate,
133+
// the caller has to update it in the source of SweepInfo (interface
134+
// SweepFetcher) and re-add the sweep by calling AddSweep.
135+
noBumping bool
130136
}
131137

132138
// rbfCache stores data related to our last fee bump.
@@ -1079,7 +1085,7 @@ func (b *batch) updateRbfRate(ctx context.Context) error {
10791085

10801086
// Set the initial value for our fee rate.
10811087
b.rbfCache.FeeRate = rate
1082-
} else {
1088+
} else if !b.cfg.noBumping {
10831089
// Bump the fee rate by the configured step.
10841090
b.rbfCache.FeeRate += defaultFeeRateStep
10851091
}

sweepbatcher/sweep_batcher.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,48 @@ type Batcher struct {
235235
// wg is a waitgroup that is used to wait for all the goroutines to
236236
// exit.
237237
wg sync.WaitGroup
238+
239+
// noBumping instructs sweepbatcher not to fee bump itself and rely on
240+
// external source of fee rates (MinFeeRate). To change the fee rate,
241+
// the caller has to update it in the source of SweepInfo (interface
242+
// SweepFetcher) and re-add the sweep by calling AddSweep.
243+
noBumping bool
244+
}
245+
246+
// BatcherConfig holds batcher configuration.
247+
type BatcherConfig struct {
248+
// noBumping instructs sweepbatcher not to fee bump itself and rely on
249+
// external source of fee rates (MinFeeRate). To change the fee rate,
250+
// the caller has to update it in the source of SweepInfo (interface
251+
// SweepFetcher) and re-add the sweep by calling AddSweep.
252+
noBumping bool
253+
}
254+
255+
// BatcherOption configures batcher behaviour.
256+
type BatcherOption func(*BatcherConfig)
257+
258+
// WithNoBumping instructs sweepbatcher not to fee bump itself and
259+
// rely on external source of fee rates (MinFeeRate). To change the
260+
// fee rate, the caller has to update it in the source of SweepInfo
261+
// (interface SweepFetcher) and re-add the sweep by calling AddSweep.
262+
func WithNoBumping() BatcherOption {
263+
return func(cfg *BatcherConfig) {
264+
cfg.noBumping = true
265+
}
238266
}
239267

240268
// NewBatcher creates a new Batcher instance.
241269
func NewBatcher(wallet lndclient.WalletKitClient,
242270
chainNotifier lndclient.ChainNotifierClient,
243271
signerClient lndclient.SignerClient, musig2ServerSigner MuSig2SignSweep,
244272
verifySchnorrSig VerifySchnorrSig, chainparams *chaincfg.Params,
245-
store BatcherStore, sweepStore SweepFetcher) *Batcher {
273+
store BatcherStore, sweepStore SweepFetcher,
274+
opts ...BatcherOption) *Batcher {
275+
276+
var cfg BatcherConfig
277+
for _, opt := range opts {
278+
opt(&cfg)
279+
}
246280

247281
return &Batcher{
248282
batches: make(map[int32]*batch),
@@ -258,6 +292,7 @@ func NewBatcher(wallet lndclient.WalletKitClient,
258292
chainParams: chainparams,
259293
store: store,
260294
sweepStore: sweepStore,
295+
noBumping: cfg.noBumping,
261296
}
262297
}
263298

@@ -416,6 +451,7 @@ func (b *Batcher) handleSweep(ctx context.Context, sweep *sweep,
416451
func (b *Batcher) spinUpBatch(ctx context.Context) (*batch, error) {
417452
cfg := batchConfig{
418453
maxTimeoutDistance: defaultMaxTimeoutDistance,
454+
noBumping: b.noBumping,
419455
}
420456

421457
switch b.chainParams {
@@ -525,6 +561,7 @@ func (b *Batcher) spinUpBatchFromDB(ctx context.Context, batch *batch) error {
525561

526562
cfg := batchConfig{
527563
maxTimeoutDistance: batch.cfg.maxTimeoutDistance,
564+
noBumping: b.noBumping,
528565
}
529566

530567
newBatch, err := NewBatchFromDB(cfg, batchKit)
@@ -587,6 +624,7 @@ func (b *Batcher) FetchUnconfirmedBatches(ctx context.Context) ([]*batch,
587624

588625
bchCfg := batchConfig{
589626
maxTimeoutDistance: bch.MaxTimeoutDistance,
627+
noBumping: b.noBumping,
590628
}
591629
batch.cfg = &bchCfg
592630

sweepbatcher/sweep_batcher_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/btcsuite/btcd/btcec/v2"
1112
"github.com/btcsuite/btcd/btcutil"
1213
"github.com/btcsuite/btcd/chaincfg/chainhash"
1314
"github.com/btcsuite/btcd/wire"
1415
"github.com/lightninglabs/loop/loopdb"
1516
"github.com/lightninglabs/loop/test"
1617
"github.com/lightninglabs/loop/utils"
1718
"github.com/lightningnetwork/lnd/chainntnfs"
19+
"github.com/lightningnetwork/lnd/keychain"
1820
"github.com/lightningnetwork/lnd/lntypes"
1921
"github.com/stretchr/testify/require"
2022
)
@@ -42,6 +44,20 @@ var destAddr = func() btcutil.Address {
4244
return addr
4345
}()
4446

47+
var senderKey, receiverKey [33]byte
48+
49+
func init() {
50+
// Generate keys.
51+
_, senderPubKey := test.CreateKey(1)
52+
copy(senderKey[:], senderPubKey.SerializeCompressed())
53+
_, receiverPubKey := test.CreateKey(2)
54+
copy(receiverKey[:], receiverPubKey.SerializeCompressed())
55+
}
56+
57+
func testVerifySchnorrSig(pubKey *btcec.PublicKey, hash, sig []byte) error {
58+
return nil
59+
}
60+
4561
func testMuSig2SignSweep(ctx context.Context,
4662
protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
4763
paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
@@ -245,6 +261,103 @@ func testSweepBatcherBatchCreation(t *testing.T, store testStore,
245261
require.True(t, batcherStore.AssertSweepStored(sweepReq3.SwapHash))
246262
}
247263

264+
// testFeeBumping tests that sweep is RBFed with slightly higher fee rate after
265+
// each block unless WithNoBumping is passed.
266+
func testFeeBumping(t *testing.T, store testStore,
267+
batcherStore testBatcherStore, noFeeBumping bool) {
268+
269+
defer test.Guard(t)()
270+
271+
lnd := test.NewMockLnd()
272+
ctx, cancel := context.WithCancel(context.Background())
273+
defer cancel()
274+
275+
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
276+
require.NoError(t, err)
277+
278+
// Disable fee bumping, if requested.
279+
var opts []BatcherOption
280+
if noFeeBumping {
281+
opts = append(opts, WithNoBumping())
282+
}
283+
284+
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
285+
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
286+
batcherStore, sweepStore, opts...)
287+
go func() {
288+
err := batcher.Run(ctx)
289+
checkBatcherError(t, err)
290+
}()
291+
292+
// Create a sweep request.
293+
sweepReq1 := SweepRequest{
294+
SwapHash: lntypes.Hash{1, 1, 1},
295+
Value: 1_000_000,
296+
Outpoint: wire.OutPoint{
297+
Hash: chainhash.Hash{1, 1},
298+
Index: 1,
299+
},
300+
Notifier: &SpendNotifier{
301+
SpendChan: make(chan *SpendDetail, ntfnBufferSize),
302+
SpendErrChan: make(chan error, ntfnBufferSize),
303+
QuitChan: make(chan bool, ntfnBufferSize),
304+
},
305+
}
306+
307+
swap1 := &loopdb.LoopOutContract{
308+
SwapContract: loopdb.SwapContract{
309+
CltvExpiry: 111,
310+
AmountRequested: 1_000_000,
311+
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
312+
HtlcKeys: loopdb.HtlcKeys{
313+
SenderScriptKey: senderKey,
314+
ReceiverScriptKey: receiverKey,
315+
SenderInternalPubKey: senderKey,
316+
ReceiverInternalPubKey: receiverKey,
317+
ClientScriptKeyLocator: keychain.KeyLocator{
318+
Family: 1,
319+
Index: 2,
320+
},
321+
},
322+
},
323+
324+
DestAddr: destAddr,
325+
SwapInvoice: swapInvoice,
326+
SweepConfTarget: 111,
327+
}
328+
329+
err = store.CreateLoopOut(ctx, sweepReq1.SwapHash, swap1)
330+
require.NoError(t, err)
331+
store.AssertLoopOutStored()
332+
333+
// Deliver sweep request to batcher.
334+
require.NoError(t, batcher.AddSweep(&sweepReq1))
335+
336+
// Since a batch was created we check that it registered for its primary
337+
// sweep's spend.
338+
<-lnd.RegisterSpendChannel
339+
340+
// Wait for tx to be published.
341+
tx1 := <-lnd.TxPublishChannel
342+
out1 := tx1.TxOut[0].Value
343+
344+
// Tick tock next block.
345+
err = lnd.NotifyHeight(601)
346+
require.NoError(t, err)
347+
348+
// Wait for another sweep tx to be published.
349+
tx2 := <-lnd.TxPublishChannel
350+
out2 := tx2.TxOut[0].Value
351+
352+
if noFeeBumping {
353+
// Expect output to stay the same.
354+
require.Equal(t, out1, out2, "expected out to stay the same")
355+
} else {
356+
// Expect output to drop.
357+
require.Greater(t, out1, out2, "expected out to drop")
358+
}
359+
}
360+
248361
// testSweepBatcherSimpleLifecycle tests the simple lifecycle of the batches
249362
// that are created and run by the batcher.
250363
func testSweepBatcherSimpleLifecycle(t *testing.T, store testStore,
@@ -1837,6 +1950,26 @@ func TestSweepBatcherBatchCreation(t *testing.T) {
18371950
runTests(t, testSweepBatcherBatchCreation)
18381951
}
18391952

1953+
// TestFeeBumping tests that sweep is RBFed with slightly higher fee rate after
1954+
// each block unless WithNoBumping is passed.
1955+
func TestFeeBumping(t *testing.T) {
1956+
t.Run("regular", func(t *testing.T) {
1957+
runTests(t, func(t *testing.T, store testStore,
1958+
batcherStore testBatcherStore) {
1959+
1960+
testFeeBumping(t, store, batcherStore, false)
1961+
})
1962+
})
1963+
1964+
t.Run("WithNoBumping", func(t *testing.T) {
1965+
runTests(t, func(t *testing.T, store testStore,
1966+
batcherStore testBatcherStore) {
1967+
1968+
testFeeBumping(t, store, batcherStore, true)
1969+
})
1970+
})
1971+
}
1972+
18401973
// TestSweepBatcherSimpleLifecycle tests the simple lifecycle of the batches
18411974
// that are created and run by the batcher.
18421975
func TestSweepBatcherSimpleLifecycle(t *testing.T) {

0 commit comments

Comments
 (0)