lnwallet+sweep: calculate max allowed feerate on BumpResult

This commit adds the method `MaxFeeRateAllowed` to calculate the max fee
rate. The caller may specify a large MaxFeeRate value, which cannot be
cover by the budget. In that case, we default to use the max feerate
calculated using `budget/weight`.
This commit is contained in:
yyforyongyu 2024-02-29 13:18:23 +08:00
parent f85661d94a
commit ecd471ac75
No known key found for this signature in database
GPG key ID: 9BCD95C4FF296868
3 changed files with 183 additions and 0 deletions

View file

@ -58,6 +58,11 @@ func (s SatPerKVByte) String() string {
// SatPerKWeight represents a fee rate in sat/kw.
type SatPerKWeight btcutil.Amount
// NewSatPerKWeight creates a new fee rate in sat/kw.
func NewSatPerKWeight(fee btcutil.Amount, weight uint64) SatPerKWeight {
return SatPerKWeight(fee.MulF64(1000 / float64(weight)))
}
// FeeForWeight calculates the fee resulting from this fee rate and the given
// weight in weight units (wu).
func (s SatPerKWeight) FeeForWeight(wu int64) btcutil.Amount {

View file

@ -88,6 +88,63 @@ type BumpRequest struct {
MaxFeeRate chainfee.SatPerKWeight
}
// MaxFeeRateAllowed returns the maximum fee rate allowed for the given
// request. It calculates the feerate using the supplied budget and the weight,
// compares it with the specified MaxFeeRate, and returns the smaller of the
// two.
func (r *BumpRequest) MaxFeeRateAllowed() (chainfee.SatPerKWeight, error) {
// Get the size of the sweep tx, which will be used to calculate the
// budget fee rate.
size, err := calcSweepTxWeight(r.Inputs, r.DeliveryAddress)
if err != nil {
return 0, err
}
// Use the budget and MaxFeeRate to decide the max allowed fee rate.
// This is needed as, when the input has a large value and the user
// sets the budget to be proportional to the input value, the fee rate
// can be very high and we need to make sure it doesn't exceed the max
// fee rate.
maxFeeRateAllowed := chainfee.NewSatPerKWeight(r.Budget, size)
if maxFeeRateAllowed > r.MaxFeeRate {
log.Debugf("Budget feerate %v exceeds MaxFeeRate %v, use "+
"MaxFeeRate instead", maxFeeRateAllowed, r.MaxFeeRate)
return r.MaxFeeRate, nil
}
log.Debugf("Budget feerate %v below MaxFeeRate %v, use budget feerate "+
"instead", maxFeeRateAllowed, r.MaxFeeRate)
return maxFeeRateAllowed, nil
}
// calcSweepTxWeight calculates the weight of the sweep tx. It assumes a
// sweeping tx always has a single output(change).
func calcSweepTxWeight(inputs []input.Input,
outputPkScript []byte) (uint64, error) {
// Use a const fee rate as we only use the weight estimator to
// calculate the size.
const feeRate = 1
// Initialize the tx weight estimator with,
// - nil outputs as we only have one single change output.
// - const fee rate as we don't care about the fees here.
// - 0 maxfeerate as we don't care about fees here.
//
// TODO(yy): we should refactor the weight estimator to not require a
// fee rate and max fee rate and make it a pure tx weight calculator.
_, estimator, err := getWeightEstimate(
inputs, nil, feeRate, 0, outputPkScript,
)
if err != nil {
return 0, err
}
return uint64(estimator.weight()), nil
}
// BumpResult is used by the Bumper to send updates about the tx being
// broadcast.
type BumpResult struct {

View file

@ -3,10 +3,24 @@ package sweep
import (
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
var (
// Create a taproot change script.
changePkScript = []byte{
0x51, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
)
// TestBumpResultValidate tests the validate method of the BumpResult struct.
func TestBumpResultValidate(t *testing.T) {
t.Parallel()
@ -50,3 +64,110 @@ func TestBumpResultValidate(t *testing.T) {
}
require.NoError(t, b.Validate())
}
// TestCalcSweepTxWeight checks that the weight of the sweep tx is calculated
// correctly.
func TestCalcSweepTxWeight(t *testing.T) {
t.Parallel()
// Create an input.
inp := createTestInput(100, input.WitnessKeyHash)
// Use a wrong change script to test the error case.
weight, err := calcSweepTxWeight([]input.Input{&inp}, []byte{0})
require.Error(t, err)
require.Zero(t, weight)
// Use a correct change script to test the success case.
weight, err = calcSweepTxWeight([]input.Input{&inp}, changePkScript)
require.NoError(t, err)
// BaseTxSize 8 bytes
// InputSize 1+41 bytes
// One P2TROutputSize 1+43 bytes
// One P2WKHWitnessSize 2+109 bytes
// Total weight = (8+42+44) * 4 + 111 = 487
require.EqualValuesf(t, 487, weight, "unexpected weight %v", weight)
}
// TestBumpRequestMaxFeeRateAllowed tests the max fee rate allowed for a bump
// request.
func TestBumpRequestMaxFeeRateAllowed(t *testing.T) {
t.Parallel()
// Create a test input.
inp := createTestInput(100, input.WitnessKeyHash)
// The weight is 487.
weight, err := calcSweepTxWeight([]input.Input{&inp}, changePkScript)
require.NoError(t, err)
// Define a test budget and calculates its fee rate.
budget := btcutil.Amount(1000)
budgetFeeRate := chainfee.NewSatPerKWeight(budget, weight)
testCases := []struct {
name string
req *BumpRequest
expectedMaxFeeRate chainfee.SatPerKWeight
expectedErr bool
}{
{
// Use a wrong change script to test the error case.
name: "error calc weight",
req: &BumpRequest{
DeliveryAddress: []byte{1},
},
expectedMaxFeeRate: 0,
expectedErr: true,
},
{
// When the budget cannot give a fee rate that matches
// the supplied MaxFeeRate, the max allowed feerate is
// capped by the budget.
name: "use budget as max fee rate",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: budget,
MaxFeeRate: budgetFeeRate + 1,
},
expectedMaxFeeRate: budgetFeeRate,
},
{
// When the budget can give a fee rate that matches the
// supplied MaxFeeRate, the max allowed feerate is
// capped by the MaxFeeRate.
name: "use config as max fee rate",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: budget,
MaxFeeRate: budgetFeeRate - 1,
},
expectedMaxFeeRate: budgetFeeRate - 1,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Check the method under test.
maxFeeRate, err := tc.req.MaxFeeRateAllowed()
// If we expect an error, check the error is returned
// and the feerate is empty.
if tc.expectedErr {
require.Error(t, err)
require.Zero(t, maxFeeRate)
return
}
// Otherwise, check the max fee rate is as expected.
require.NoError(t, err)
require.Equal(t, tc.expectedMaxFeeRate, maxFeeRate)
})
}
}