lnd.xprv/lnwallet/wallet_test.go
Olaoluwa Osuntokun 777ed104a3
chainfee: create new chainfee package extracting fees from lnwallet
In this commit, we create a new chainfee package, that houses all fee
related functionality used within the codebase. The creation of this new
package furthers our long-term goal of extracting functionality from the
bloated `lnwallet` package into new distinct packages. Additionally,
this new packages resolves a class of import cycle that could arise if a
new package that was imported by something in `lnwallet` wanted to use
the existing fee related functions in the prior `lnwallet` package.
2019-10-31 16:41:57 -07:00

354 lines
9.2 KiB
Go

package lnwallet
import (
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// fundingFee is a helper method that returns the fee estimate used for a tx
// with the given number of inputs and the optional change output. This matches
// the estimate done by the wallet.
func fundingFee(feeRate chainfee.SatPerKWeight, numInput int, // nolint:unparam
change bool) btcutil.Amount {
var weightEstimate input.TxWeightEstimator
// All inputs.
for i := 0; i < numInput; i++ {
weightEstimate.AddP2WKHInput()
}
// The multisig funding output.
weightEstimate.AddP2WSHOutput()
// Optionally count a change output.
if change {
weightEstimate.AddP2WKHOutput()
}
totalWeight := int64(weightEstimate.Weight())
return feeRate.FeeForWeight(totalWeight)
}
// TestCoinSelect tests that we pick coins adding up to the expected amount
// when creating a funding transaction, and that the calculated change is the
// expected amount.
//
// NOTE: coinSelect will always attempt to add a change output, so we must
// account for this in the tests.
func TestCoinSelect(t *testing.T) {
t.Parallel()
const feeRate = chainfee.SatPerKWeight(100)
const dust = btcutil.Amount(100)
type testCase struct {
name string
outputValue btcutil.Amount
coins []*Utxo
expectedInput []btcutil.Amount
expectedChange btcutil.Amount
expectErr bool
}
testCases := []testCase{
{
// We have 1.0 BTC available, and wants to send 0.5.
// This will obviously lead to a change output of
// almost 0.5 BTC.
name: "big change",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{
1 * btcutil.SatoshiPerBitcoin,
},
// Change will be what's left minus the fee.
expectedChange: 0.5*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
},
{
// We have 1 BTC available, and we want to send 1 BTC.
// This should lead to an error, as we don't have
// enough funds to pay the fee.
name: "nothing left for fees",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
outputValue: 1 * btcutil.SatoshiPerBitcoin,
expectErr: true,
},
{
// We have a 1 BTC input, and want to create an output
// as big as possible, such that the remaining change
// will be dust.
name: "dust change",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
// We tune the output value by subtracting the expected
// fee and a small dust amount.
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
expectedInput: []btcutil.Amount{
1 * btcutil.SatoshiPerBitcoin,
},
// Change will the dust.
expectedChange: dust,
},
{
// We have a 1 BTC input, and want to create an output
// as big as possible, such that there is nothing left
// for change.
name: "no change",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
// We tune the output value to be the maximum amount
// possible, leaving just enough for fees.
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
expectedInput: []btcutil.Amount{
1 * btcutil.SatoshiPerBitcoin,
},
// We have just enough left to pay the fee, so there is
// nothing left for change.
// TODO(halseth): currently coinselect estimates fees
// assuming a change output.
expectedChange: 0,
},
}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
selected, changeAmt, err := coinSelect(
feeRate, test.outputValue, test.coins,
)
if !test.expectErr && err != nil {
t.Fatalf(err.Error())
}
if test.expectErr && err == nil {
t.Fatalf("expected error")
}
// If we got an expected error, there is nothing more to test.
if test.expectErr {
return
}
// Check that the selected inputs match what we expect.
if len(selected) != len(test.expectedInput) {
t.Fatalf("expected %v inputs, got %v",
len(test.expectedInput), len(selected))
}
for i, coin := range selected {
if coin.Value != test.expectedInput[i] {
t.Fatalf("expected input %v to have value %v, "+
"had %v", i, test.expectedInput[i],
coin.Value)
}
}
// Assert we got the expected change amount.
if changeAmt != test.expectedChange {
t.Fatalf("expected %v change amt, got %v",
test.expectedChange, changeAmt)
}
})
}
}
// TestCoinSelectSubtractFees tests that we pick coins adding up to the
// expected amount when creating a funding transaction, and that a change
// output is created only when necessary.
func TestCoinSelectSubtractFees(t *testing.T) {
t.Parallel()
const feeRate = chainfee.SatPerKWeight(100)
const dustLimit = btcutil.Amount(1000)
const dust = btcutil.Amount(100)
type testCase struct {
name string
spendValue btcutil.Amount
coins []*Utxo
expectedInput []btcutil.Amount
expectedFundingAmt btcutil.Amount
expectedChange btcutil.Amount
expectErr bool
}
testCases := []testCase{
{
// We have 1.0 BTC available, spend them all. This
// should lead to a funding TX with one output, the
// rest goes to fees.
name: "spend all",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
spendValue: 1 * btcutil.SatoshiPerBitcoin,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{
1 * btcutil.SatoshiPerBitcoin,
},
expectedFundingAmt: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, false),
expectedChange: 0,
},
{
// The total funds available is below the dust limit
// after paying fees.
name: "dust output",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: fundingFee(feeRate, 1, false) + dust,
},
},
spendValue: fundingFee(feeRate, 1, false) + dust,
expectErr: true,
},
{
// After subtracting fees, the resulting change output
// is below the dust limit. The remainder should go
// towards the funding output.
name: "dust change",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 1 * btcutil.SatoshiPerBitcoin,
},
},
spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
expectedInput: []btcutil.Amount{
1 * btcutil.SatoshiPerBitcoin,
},
expectedFundingAmt: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, false),
expectedChange: 0,
},
{
// We got just enough funds to create an output above the dust limit.
name: "output right above dustlimit",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: fundingFee(feeRate, 1, false) + dustLimit + 1,
},
},
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
expectedInput: []btcutil.Amount{
fundingFee(feeRate, 1, false) + dustLimit + 1,
},
expectedFundingAmt: dustLimit + 1,
expectedChange: 0,
},
{
// Amount left is below dust limit after paying fee for
// a change output, resulting in a no-change tx.
name: "no amount to pay fee for change",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: fundingFee(feeRate, 1, false) + 2*(dustLimit+1),
},
},
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
expectedInput: []btcutil.Amount{
fundingFee(feeRate, 1, false) + 2*(dustLimit+1),
},
expectedFundingAmt: 2 * (dustLimit + 1),
expectedChange: 0,
},
{
// If more than 20% of funds goes to fees, it should fail.
name: "high fee",
coins: []*Utxo{
{
AddressType: WitnessPubKey,
Value: 5 * fundingFee(feeRate, 1, false),
},
},
spendValue: 5 * fundingFee(feeRate, 1, false),
expectErr: true,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
selected, localFundingAmt, changeAmt, err := coinSelectSubtractFees(
feeRate, test.spendValue, dustLimit, test.coins,
)
if !test.expectErr && err != nil {
t.Fatalf(err.Error())
}
if test.expectErr && err == nil {
t.Fatalf("expected error")
}
// If we got an expected error, there is nothing more to test.
if test.expectErr {
return
}
// Check that the selected inputs match what we expect.
if len(selected) != len(test.expectedInput) {
t.Fatalf("expected %v inputs, got %v",
len(test.expectedInput), len(selected))
}
for i, coin := range selected {
if coin.Value != test.expectedInput[i] {
t.Fatalf("expected input %v to have value %v, "+
"had %v", i, test.expectedInput[i],
coin.Value)
}
}
// Assert we got the expected change amount.
if localFundingAmt != test.expectedFundingAmt {
t.Fatalf("expected %v local funding amt, got %v",
test.expectedFundingAmt, localFundingAmt)
}
if changeAmt != test.expectedChange {
t.Fatalf("expected %v change amt, got %v",
test.expectedChange, changeAmt)
}
})
}
}