Skip to content

Commit 864d7f2

Browse files
committed
sweepbatcher: make sure feerate does not decrease
Previous behaviour was to overwrite batch's feerate with minFeeRate of its primary sweep, which could be lower that previus batch's feerate or lower that feerate of some other sweep. Instead, batch's feerate only grows and never declines and is at least as high as the highest feerate of its sweeps. Added a test to verify this.
1 parent f0a25f9 commit 864d7f2

File tree

2 files changed

+189
-1
lines changed

2 files changed

+189
-1
lines changed

sweepbatcher/sweep_batch.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,10 +470,18 @@ func (b *batch) addSweep(ctx context.Context, sweep *sweep) (bool, error) {
470470
// batch's confirmation target and fee rate.
471471
if b.primarySweepID == sweep.swapHash {
472472
b.cfg.batchConfTarget = sweep.confTarget
473-
b.rbfCache.FeeRate = sweep.minFeeRate
474473
b.rbfCache.SkipNextBump = true
475474
}
476475

476+
// Update batch's fee rate to be greater than or equal to
477+
// minFeeRate of the sweep. Make sure batch's fee rate does not
478+
// decrease (otherwise it won't pass RBF rules and won't be
479+
// broadcasted) and that it is not lower that minFeeRate of
480+
// other sweeps (so it is applied).
481+
if b.rbfCache.FeeRate < sweep.minFeeRate {
482+
b.rbfCache.FeeRate = sweep.minFeeRate
483+
}
484+
477485
return true, nil
478486
}
479487

sweepbatcher/sweep_batcher_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3698,6 +3698,180 @@ func testWithMixedBatchCoopFailedOnly(t *testing.T, store testStore,
36983698
wantWeight, wantWitnessSizes)
36993699
}
37003700

3701+
// testFeeRateGrows tests that fee rate of a batch does not decrease and is at
3702+
// least as high as the highest fee rate of sweeps.
3703+
func testFeeRateGrows(t *testing.T, store testStore,
3704+
batcherStore testBatcherStore) {
3705+
3706+
defer test.Guard(t)()
3707+
3708+
lnd := test.NewMockLnd()
3709+
ctx, cancel := context.WithCancel(context.Background())
3710+
defer cancel()
3711+
3712+
sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
3713+
require.NoError(t, err)
3714+
3715+
// Create a map to store fee rates.
3716+
swap2feeRate := map[lntypes.Hash]chainfee.SatPerKWeight{}
3717+
var swap2feeRateMu sync.Mutex
3718+
setFeeRate := func(swapHash lntypes.Hash, rate chainfee.SatPerKWeight) {
3719+
swap2feeRateMu.Lock()
3720+
defer swap2feeRateMu.Unlock()
3721+
3722+
swap2feeRate[swapHash] = rate
3723+
}
3724+
3725+
customFeeRate := func(ctx context.Context,
3726+
swapHash lntypes.Hash) (chainfee.SatPerKWeight, error) {
3727+
3728+
swap2feeRateMu.Lock()
3729+
defer swap2feeRateMu.Unlock()
3730+
3731+
return swap2feeRate[swapHash], nil
3732+
}
3733+
3734+
const (
3735+
feeRateLow = chainfee.SatPerKWeight(10_000)
3736+
feeRateMedium = chainfee.SatPerKWeight(30_000)
3737+
feeRateHigh = chainfee.SatPerKWeight(50_000)
3738+
)
3739+
3740+
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
3741+
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
3742+
batcherStore, sweepStore, WithCustomFeeRate(customFeeRate))
3743+
3744+
go func() {
3745+
err := batcher.Run(ctx)
3746+
checkBatcherError(t, err)
3747+
}()
3748+
3749+
// Create the first sweep.
3750+
swapHash1 := lntypes.Hash{1, 1, 1}
3751+
setFeeRate(swapHash1, feeRateMedium)
3752+
sweepReq1 := SweepRequest{
3753+
SwapHash: swapHash1,
3754+
Value: 1_000_000,
3755+
Outpoint: wire.OutPoint{
3756+
Hash: chainhash.Hash{1, 1},
3757+
Index: 1,
3758+
},
3759+
Notifier: &dummyNotifier,
3760+
}
3761+
3762+
swap1 := &loopdb.LoopOutContract{
3763+
SwapContract: loopdb.SwapContract{
3764+
CltvExpiry: 111,
3765+
AmountRequested: 1_000_000,
3766+
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
3767+
HtlcKeys: htlcKeys,
3768+
3769+
// Make preimage unique to pass SQL constraints.
3770+
Preimage: lntypes.Preimage{1},
3771+
},
3772+
3773+
DestAddr: destAddr,
3774+
SwapInvoice: swapInvoice,
3775+
SweepConfTarget: 111,
3776+
}
3777+
3778+
err = store.CreateLoopOut(ctx, swapHash1, swap1)
3779+
require.NoError(t, err)
3780+
store.AssertLoopOutStored()
3781+
3782+
// Deliver sweep request to batcher.
3783+
require.NoError(t, batcher.AddSweep(&sweepReq1))
3784+
3785+
// Since a batch was created we check that it registered for its primary
3786+
// sweep's spend.
3787+
<-lnd.RegisterSpendChannel
3788+
3789+
// Wait for tx to be published.
3790+
<-lnd.TxPublishChannel
3791+
3792+
// Make sure the fee rate is feeRateMedium.
3793+
batch := getOnlyBatch(batcher)
3794+
require.Len(t, batch.sweeps, 1)
3795+
require.Equal(t, feeRateMedium, batch.rbfCache.FeeRate)
3796+
3797+
// Now decrease the fee of sweep1.
3798+
setFeeRate(swapHash1, feeRateLow)
3799+
require.NoError(t, batcher.AddSweep(&sweepReq1))
3800+
3801+
// Tick tock next block.
3802+
err = lnd.NotifyHeight(601)
3803+
require.NoError(t, err)
3804+
3805+
// Wait for tx to be published.
3806+
<-lnd.TxPublishChannel
3807+
3808+
// Make sure the fee rate is still feeRateMedium.
3809+
require.Equal(t, feeRateMedium, batch.rbfCache.FeeRate)
3810+
3811+
// Add sweep2, with feeRateMedium.
3812+
swapHash2 := lntypes.Hash{2, 2, 2}
3813+
setFeeRate(swapHash2, feeRateMedium)
3814+
sweepReq2 := SweepRequest{
3815+
SwapHash: swapHash2,
3816+
Value: 1_000_000,
3817+
Outpoint: wire.OutPoint{
3818+
Hash: chainhash.Hash{2, 2},
3819+
Index: 1,
3820+
},
3821+
Notifier: &dummyNotifier,
3822+
}
3823+
3824+
swap2 := &loopdb.LoopOutContract{
3825+
SwapContract: loopdb.SwapContract{
3826+
CltvExpiry: 111,
3827+
AmountRequested: 1_000_000,
3828+
ProtocolVersion: loopdb.ProtocolVersionMuSig2,
3829+
HtlcKeys: htlcKeys,
3830+
3831+
// Make preimage unique to pass SQL constraints.
3832+
Preimage: lntypes.Preimage{2},
3833+
},
3834+
3835+
DestAddr: destAddr,
3836+
SwapInvoice: swapInvoice,
3837+
SweepConfTarget: 111,
3838+
}
3839+
3840+
err = store.CreateLoopOut(ctx, swapHash2, swap2)
3841+
require.NoError(t, err)
3842+
store.AssertLoopOutStored()
3843+
3844+
// Deliver sweep request to batcher.
3845+
require.NoError(t, batcher.AddSweep(&sweepReq2))
3846+
3847+
// Tick tock next block.
3848+
err = lnd.NotifyHeight(602)
3849+
require.NoError(t, err)
3850+
3851+
// Wait for tx to be published.
3852+
<-lnd.TxPublishChannel
3853+
3854+
// Make sure the fee rate is still feeRateMedium.
3855+
require.Len(t, batch.sweeps, 2)
3856+
require.Equal(t, feeRateMedium, batch.rbfCache.FeeRate)
3857+
3858+
// Now update fee rate of second sweep (which is not primary) to
3859+
// feeRateHigh. Fee rate of sweep 1 is still feeRateLow.
3860+
setFeeRate(swapHash2, feeRateHigh)
3861+
require.NoError(t, batcher.AddSweep(&sweepReq1))
3862+
require.NoError(t, batcher.AddSweep(&sweepReq2))
3863+
3864+
// Tick tock next block.
3865+
err = lnd.NotifyHeight(603)
3866+
require.NoError(t, err)
3867+
3868+
// Wait for tx to be published.
3869+
<-lnd.TxPublishChannel
3870+
3871+
// Make sure the fee rate increased to feeRateHigh.
3872+
require.Equal(t, feeRateHigh, batch.rbfCache.FeeRate)
3873+
}
3874+
37013875
// TestSweepBatcherBatchCreation tests that sweep requests enter the expected
37023876
// batch based on their timeout distance.
37033877
func TestSweepBatcherBatchCreation(t *testing.T) {
@@ -3839,6 +4013,12 @@ func TestWithMixedBatchCoopFailedOnly(t *testing.T) {
38394013
runTests(t, testWithMixedBatchCoopFailedOnly)
38404014
}
38414015

4016+
// TestFeeRateGrows tests that fee rate of a batch does not decrease and is at
4017+
// least as high as the highest fee rate of sweeps.
4018+
func TestFeeRateGrows(t *testing.T) {
4019+
runTests(t, testFeeRateGrows)
4020+
}
4021+
38424022
// testBatcherStore is BatcherStore used in tests.
38434023
type testBatcherStore interface {
38444024
BatcherStore

0 commit comments

Comments
 (0)