chanfunding+lnwallet: move coin selection code into new chanfunding package
In this commit, we make an incremental change to move the existing coin selection code into a new chanfunding package. In later commits, this package will grow to serve all the lower level channel funding needs in the daemon.
This commit is contained in:
3 files changed
+299
-228
No files matched your search
@@ -1,353 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user