input+sweep: make sure input with no fee rate is not added to cluster
This commit makes sure an input is only added to the cluster when it has successfully estimated its fee rate. Previously, when an error is returned from `feeRateForPreference`, we'd still add this input to the cluster, resulting a **lower** fee rates being used because when averaging the fee rates, we'd think this input has zero fee rate specified. An unit test is patched to make the method `clusterByLockTime` more robust.
This commit is contained in:
3 files changed
+317
-4
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -2357,3 +2358,182 @@ func TestFeeRateForPreference(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClusterByLockTime tests the method clusterByLockTime works as expected.
|
||||
func TestClusterByLockTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test param with a dummy fee preference. This is needed so
|
||||
// `feeRateForPreference` won't throw an error.
|
||||
param := Params{Fee: FeePreference{ConfTarget: 1}}
|
||||
|
||||
// We begin the test by creating three clusters of inputs, the first
|
||||
// cluster has a locktime of 1, the second has a locktime of 2, and the
|
||||
// final has no locktime.
|
||||
lockTime1 := uint32(1)
|
||||
lockTime2 := uint32(2)
|
||||
|
||||
// Create cluster one, which has a locktime of 1.
|
||||
input1LockTime1 := &input.MockInput{}
|
||||
input2LockTime1 := &input.MockInput{}
|
||||
input1LockTime1.On("RequiredLockTime").Return(lockTime1, true)
|
||||
input2LockTime1.On("RequiredLockTime").Return(lockTime1, true)
|
||||
|
||||
// Create cluster two, which has a locktime of 2.
|
||||
input3LockTime2 := &input.MockInput{}
|
||||
input4LockTime2 := &input.MockInput{}
|
||||
input3LockTime2.On("RequiredLockTime").Return(lockTime2, true)
|
||||
input4LockTime2.On("RequiredLockTime").Return(lockTime2, true)
|
||||
|
||||
// Create cluster three, which has no locktime.
|
||||
input5NoLockTime := &input.MockInput{}
|
||||
input6NoLockTime := &input.MockInput{}
|
||||
input5NoLockTime.On("RequiredLockTime").Return(uint32(0), false)
|
||||
input6NoLockTime.On("RequiredLockTime").Return(uint32(0), false)
|
||||
|
||||
// With the inner Input being mocked, we can now create the pending
|
||||
// inputs.
|
||||
input1 := &pendingInput{Input: input1LockTime1, params: param}
|
||||
input2 := &pendingInput{Input: input2LockTime1, params: param}
|
||||
input3 := &pendingInput{Input: input3LockTime2, params: param}
|
||||
input4 := &pendingInput{Input: input4LockTime2, params: param}
|
||||
input5 := &pendingInput{Input: input5NoLockTime, params: param}
|
||||
input6 := &pendingInput{Input: input6NoLockTime, params: param}
|
||||
|
||||
// Create the pending inputs map, which will be passed to the method
|
||||
// under test.
|
||||
//
|
||||
// NOTE: we don't care the actual outpoint values as long as they are
|
||||
// unique.
|
||||
inputs := pendingInputs{
|
||||
wire.OutPoint{Index: 1}: input1,
|
||||
wire.OutPoint{Index: 2}: input2,
|
||||
wire.OutPoint{Index: 3}: input3,
|
||||
wire.OutPoint{Index: 4}: input4,
|
||||
wire.OutPoint{Index: 5}: input5,
|
||||
wire.OutPoint{Index: 6}: input6,
|
||||
}
|
||||
|
||||
// Create expected clusters so we can shorten the line length in the
|
||||
// test cases below.
|
||||
cluster1 := pendingInputs{
|
||||
wire.OutPoint{Index: 1}: input1,
|
||||
wire.OutPoint{Index: 2}: input2,
|
||||
}
|
||||
cluster2 := pendingInputs{
|
||||
wire.OutPoint{Index: 3}: input3,
|
||||
wire.OutPoint{Index: 4}: input4,
|
||||
}
|
||||
|
||||
// cluster3 should be the remaining inputs since they don't have
|
||||
// locktime.
|
||||
cluster3 := pendingInputs{
|
||||
wire.OutPoint{Index: 5}: input5,
|
||||
wire.OutPoint{Index: 6}: input6,
|
||||
}
|
||||
|
||||
// Set the min fee rate to be 1000 sat/kw.
|
||||
const minFeeRate = chainfee.SatPerKWeight(1000)
|
||||
|
||||
// Create a test sweeper.
|
||||
s := New(&UtxoSweeperConfig{
|
||||
MaxFeeRate: minFeeRate.FeePerVByte() * 10,
|
||||
})
|
||||
|
||||
// Set the relay fee to be the minFeeRate. Any fee rate below the
|
||||
// minFeeRate will cause an error to be returned.
|
||||
s.relayFeeRate = minFeeRate
|
||||
|
||||
// applyFeeRate takes a testing fee rate and makes a mocker over
|
||||
// DetermineFeePerKw that always return the testing fee rate. This
|
||||
// mocked method is then attached to the sweeper.
|
||||
applyFeeRate := func(feeRate chainfee.SatPerKWeight) {
|
||||
mockFeeFunc := func(_ chainfee.Estimator, _ FeePreference) (
|
||||
chainfee.SatPerKWeight, error) {
|
||||
|
||||
return feeRate, nil
|
||||
}
|
||||
|
||||
s.cfg.DetermineFeePerKw = mockFeeFunc
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFeeRate chainfee.SatPerKWeight
|
||||
expectedClusters []inputCluster
|
||||
expectedRemainingInputs pendingInputs
|
||||
}{
|
||||
{
|
||||
// Test a successful case where the locktime clusters
|
||||
// are created and the no-locktime cluster is returned
|
||||
// as the remaining inputs.
|
||||
name: "successfully create clusters",
|
||||
// Use a fee rate above the min value so we don't hit
|
||||
// an error when performing fee estimation.
|
||||
//
|
||||
// TODO(yy): we should customize the returned fee rate
|
||||
// for each input to further test the averaging logic.
|
||||
// Or we can split the method into two, one for
|
||||
// grouping the clusters and the other for averaging
|
||||
// the fee rates so it's easier to be tested.
|
||||
testFeeRate: minFeeRate + 1,
|
||||
expectedClusters: []inputCluster{
|
||||
{
|
||||
lockTime: &lockTime1,
|
||||
sweepFeeRate: minFeeRate + 1,
|
||||
inputs: cluster1,
|
||||
},
|
||||
{
|
||||
lockTime: &lockTime2,
|
||||
sweepFeeRate: minFeeRate + 1,
|
||||
inputs: cluster2,
|
||||
},
|
||||
},
|
||||
expectedRemainingInputs: cluster3,
|
||||
},
|
||||
{
|
||||
// Test that when the input is skipped when the fee
|
||||
// estimation returns an error.
|
||||
name: "error from fee estimation",
|
||||
// Use a fee rate below the min value so we hit an
|
||||
// error when performing fee estimation.
|
||||
testFeeRate: minFeeRate - 1,
|
||||
expectedClusters: []inputCluster{},
|
||||
// Remaining inputs should stay untouched.
|
||||
expectedRemainingInputs: cluster3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Apply the test fee rate so `feeRateForPreference` is
|
||||
// mocked to return the specified value.
|
||||
applyFeeRate(tc.testFeeRate)
|
||||
|
||||
// Call the method under test.
|
||||
clusters, remainingInputs := s.clusterByLockTime(inputs)
|
||||
|
||||
// Sort by locktime as the order is not guaranteed.
|
||||
sort.Slice(clusters, func(i, j int) bool {
|
||||
return *clusters[i].lockTime <
|
||||
*clusters[j].lockTime
|
||||
})
|
||||
|
||||
// Validate the values are returned as expected.
|
||||
require.Equal(t, tc.expectedClusters, clusters)
|
||||
require.Equal(t, tc.expectedRemainingInputs,
|
||||
remainingInputs,
|
||||
)
|
||||
|
||||
// Assert the mocked methods are called as expeceted.
|
||||
input1LockTime1.AssertExpectations(t)
|
||||
input2LockTime1.AssertExpectations(t)
|
||||
input3LockTime2.AssertExpectations(t)
|
||||
input4LockTime2.AssertExpectations(t)
|
||||
input5NoLockTime.AssertExpectations(t)
|
||||
input6NoLockTime.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user