chanfunding: fee estimation based on change output in CoinSelect
Add a dust-limit to `CoinSelect` and let the fee estimation consider if a change output is needed or not, assuring that if the change is below the dust-limit it will go towards the fee instead.
This commit is contained in:
parent
235e73205d
commit
59c40ec8b4
@ -122,7 +122,7 @@ func sanityCheckFee(totalOut, fee btcutil.Amount) error {
|
|||||||
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
||||||
// specified fee rate should be expressed in sat/kw for coin selection to
|
// specified fee rate should be expressed in sat/kw for coin selection to
|
||||||
// function properly.
|
// function properly.
|
||||||
func CoinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
func CoinSelect(feeRate chainfee.SatPerKWeight, amt, dustLimit btcutil.Amount,
|
||||||
coins []Coin) ([]Coin, btcutil.Amount, error) {
|
coins []Coin) ([]Coin, btcutil.Amount, error) {
|
||||||
|
|
||||||
amtNeeded := amt
|
amtNeeded := amt
|
||||||
@ -136,29 +136,55 @@ func CoinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
|||||||
|
|
||||||
// Obtain fee estimates both with and without using a change
|
// Obtain fee estimates both with and without using a change
|
||||||
// output.
|
// output.
|
||||||
_, requiredFeeWithChange, err := calculateFees(
|
requiredFeeNoChange, requiredFeeWithChange, err := calculateFees(
|
||||||
selectedUtxos, feeRate,
|
selectedUtxos, feeRate,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The difference between the selected amount and the amount
|
// The difference between the selected amount and the amount
|
||||||
// requested will be used to pay fees, and generate a change
|
// requested will be used to pay fees, and generate a change
|
||||||
// output with the remaining.
|
// output with the remaining.
|
||||||
overShootAmt := totalSat - amt
|
overShootAmt := totalSat - amt
|
||||||
|
|
||||||
// Based on the estimated size and fee rate, if the excess
|
var changeAmt btcutil.Amount
|
||||||
// amount isn't enough to pay fees, then increase the requested
|
|
||||||
// coin amount by the estimate required fee, performing another
|
switch {
|
||||||
// round of coin selection.
|
|
||||||
if overShootAmt < requiredFeeWithChange {
|
// If the excess amount isn't enough to pay for fees based on
|
||||||
amtNeeded = amt + requiredFeeWithChange
|
// fee rate and estimated size without using a change output,
|
||||||
|
// then increase the requested coin amount by the estimate
|
||||||
|
// required fee without using change, performing another round
|
||||||
|
// of coin selection.
|
||||||
|
case overShootAmt < requiredFeeNoChange:
|
||||||
|
amtNeeded = amt + requiredFeeNoChange
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
// If sufficient funds were selected to cover the fee required
|
||||||
|
// to include a change output, the remainder will be our change
|
||||||
|
// amount.
|
||||||
|
case overShootAmt > requiredFeeWithChange:
|
||||||
|
changeAmt = overShootAmt - requiredFeeWithChange
|
||||||
|
|
||||||
|
// Otherwise we have selected enough to pay for a tx without a
|
||||||
|
// change output.
|
||||||
|
default:
|
||||||
|
changeAmt = 0
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the fee is sufficient, then calculate the size of the
|
if changeAmt < dustLimit {
|
||||||
// change output.
|
changeAmt = 0
|
||||||
changeAmt := overShootAmt - requiredFeeWithChange
|
}
|
||||||
|
|
||||||
|
// Sanity check the resulting output values to make sure we
|
||||||
|
// don't burn a great part to fees.
|
||||||
|
totalOut := amt + changeAmt
|
||||||
|
err = sanityCheckFee(totalOut, totalSat-totalOut)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
return selectedUtxos, changeAmt, nil
|
return selectedUtxos, changeAmt, nil
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const feeRate = chainfee.SatPerKWeight(100)
|
const feeRate = chainfee.SatPerKWeight(100)
|
||||||
const dust = btcutil.Amount(100)
|
const dustLimit = btcutil.Amount(1000)
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
@ -197,7 +197,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// We have a 1 BTC input, and want to create an output
|
// We have a 1 BTC input, and want to create an output
|
||||||
// as big as possible, such that the remaining change
|
// as big as possible, such that the remaining change
|
||||||
// will be dust.
|
// would be dust but instead goes to fees.
|
||||||
name: "dust change",
|
name: "dust change",
|
||||||
coins: []Coin{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
@ -208,41 +208,53 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// We tune the output value by subtracting the expected
|
// We tune the output value by subtracting the expected
|
||||||
// fee and a small dust amount.
|
// fee and the dustlimit.
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
|
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, false) - dustLimit,
|
||||||
|
|
||||||
expectedInput: []btcutil.Amount{
|
expectedInput: []btcutil.Amount{
|
||||||
1 * btcutil.SatoshiPerBitcoin,
|
1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Change will the dust.
|
// Change must be zero.
|
||||||
expectedChange: dust,
|
expectedChange: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// We have a 1 BTC input, and want to create an output
|
// We got just enough funds to create a change output above the
|
||||||
// as big as possible, such that there is nothing left
|
// dust limit.
|
||||||
// for change.
|
name: "change right above dustlimit",
|
||||||
name: "no change",
|
|
||||||
coins: []Coin{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
TxOut: wire.TxOut{
|
TxOut: wire.TxOut{
|
||||||
PkScript: p2wkhScript,
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: int64(fundingFee(feeRate, 1, true) + 2*(dustLimit+1)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// We tune the output value to be the maximum amount
|
// We tune the output value to be just above the dust limit.
|
||||||
// possible, leaving just enough for fees.
|
outputValue: dustLimit + 1,
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
|
|
||||||
|
|
||||||
expectedInput: []btcutil.Amount{
|
expectedInput: []btcutil.Amount{
|
||||||
1 * btcutil.SatoshiPerBitcoin,
|
fundingFee(feeRate, 1, true) + 2*(dustLimit+1),
|
||||||
},
|
},
|
||||||
// We have just enough left to pay the fee, so there is
|
|
||||||
// nothing left for change.
|
// After paying for the fee the change output should be just above
|
||||||
// TODO(halseth): currently coinselect estimates fees
|
// the dust limit.
|
||||||
// assuming a change output.
|
expectedChange: dustLimit + 1,
|
||||||
expectedChange: 0,
|
},
|
||||||
|
{
|
||||||
|
// If more than 20% of funds goes to fees, it should fail.
|
||||||
|
name: "high fee",
|
||||||
|
coins: []Coin{
|
||||||
|
{
|
||||||
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(5 * fundingFee(feeRate, 1, false)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputValue: 4 * fundingFee(feeRate, 1, false),
|
||||||
|
|
||||||
|
expectErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +264,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
selected, changeAmt, err := CoinSelect(
|
selected, changeAmt, err := CoinSelect(
|
||||||
feeRate, test.outputValue, test.coins,
|
feeRate, test.outputValue, dustLimit, test.coins,
|
||||||
)
|
)
|
||||||
if !test.expectErr && err != nil {
|
if !test.expectErr && err != nil {
|
||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
|
@ -289,9 +289,10 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
|
|||||||
// Otherwise do a normal coin selection where we target a given
|
// Otherwise do a normal coin selection where we target a given
|
||||||
// funding amount.
|
// funding amount.
|
||||||
default:
|
default:
|
||||||
|
dustLimit := w.cfg.DustLimit
|
||||||
localContributionAmt = r.LocalAmt
|
localContributionAmt = r.LocalAmt
|
||||||
selectedCoins, changeAmt, err = CoinSelect(
|
selectedCoins, changeAmt, err = CoinSelect(
|
||||||
r.FeeRate, r.LocalAmt, coins,
|
r.FeeRate, r.LocalAmt, dustLimit, coins,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
Loading…
Reference in New Issue
Block a user