mod+chanfunding: use coin selection strategy for channel funding

The wallet assembler is now aware of the node config level coin
selection strategy, so we can use it when creating new channels.
This commit is contained in:
Oliver Gugger 2024-02-06 12:25:47 +01:00
parent f7198c4105
commit 9bdddbcc56
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
8 changed files with 131 additions and 114 deletions

12
go.mod
View File

@ -10,11 +10,11 @@ require (
github.com/btcsuite/btcd/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcwallet v0.16.10-0.20240127010340-16b422a2e8bf
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0
github.com/btcsuite/btcwallet/walletdb v1.4.0
github.com/btcsuite/btcwallet/wtxmgr v1.5.0
github.com/btcsuite/btcwallet v0.16.10-0.20240206195028-1f3534b00d14
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4
github.com/btcsuite/btcwallet/wallet/txrules v1.2.1
github.com/btcsuite/btcwallet/walletdb v1.4.1
github.com/btcsuite/btcwallet/wtxmgr v1.5.1
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
@ -77,7 +77,7 @@ require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect

32
go.sum
View File

@ -71,19 +71,15 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g=
github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.1-0.20240123000108-62e6af035ec5 h1:8BHBWvtP6kkzvmCpyWEznq4eS0gfLOSVuXLesv413Xs=
github.com/btcsuite/btcd v0.24.1-0.20240123000108-62e6af035ec5/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34=
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg=
@ -95,20 +91,18 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcwallet v0.16.10-0.20240127010340-16b422a2e8bf h1:eNjj5R0tKP48NQxDkuKr+C9frZsdzTAemEwu75ZDQg0=
github.com/btcsuite/btcwallet v0.16.10-0.20240127010340-16b422a2e8bf/go.mod h1:LzcW/LYkQLgDufv6Ouw4cOIW0YsY+A60MTtc61/OZTU=
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 h1:etuLgGEojecsDOYTII8rYiGHjGyV5xTqsXi+ZQ715UU=
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2/go.mod h1:Zpk/LOb2sKqwP2lmHjaZT9AdaKsHPSbNLm2Uql5IQ/0=
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg=
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0/go.mod h1:AtkqiL7ccKWxuLYtZm8Bu8G6q82w4yIZdgq6riy60z0=
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.2/go.mod h1:q08Rms52VyWyXcp5zDc4tdFRKkFgNsMQrv3/LvE1448=
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 h1:PszOub7iXVYbtGybym5TGCp9Dv1h1iX4rIC3HICZGLg=
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3/go.mod h1:q08Rms52VyWyXcp5zDc4tdFRKkFgNsMQrv3/LvE1448=
github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ=
github.com/btcsuite/btcwallet/walletdb v1.4.0/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 h1:WO0KyN4l6H3JWnlFxfGR7r3gDnlGT7W2cL8vl6av4SU=
github.com/btcsuite/btcwallet/wtxmgr v1.5.0/go.mod h1:TQVDhFxseiGtZwEPvLgtfyxuNUDsIdaJdshvWzR0HJ4=
github.com/btcsuite/btcwallet v0.16.10-0.20240206195028-1f3534b00d14 h1:GnTInK5UIkDJw9alXR4JeOmKmodJu/5qYknZWqp1Sk0=
github.com/btcsuite/btcwallet v0.16.10-0.20240206195028-1f3534b00d14/go.mod h1:3EItwIQcrXGkcQlO1OginQ3Ab8YgE8kxka9hjgFuWxM=
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 h1:poyHFf7+5+RdxNp5r2T6IBRD7RyraUsYARYbp/7t4D8=
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4/go.mod h1:GETGDQuyq+VFfH1S/+/7slLM/9aNa4l7P4ejX6dJfb0=
github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 h1:UZo7YRzdHbwhK7Rhv3PO9bXgTxiOH45edK5qdsdiatk=
github.com/btcsuite/btcwallet/wallet/txrules v1.2.1/go.mod h1:MVSqRkju/IGxImXYPfBkG65FgEZYA4fXchheILMVl8g=
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 h1:nmcKAVTv/cmYrs0A4hbiC6Qw+WTLYy/14SmTt3mLnCo=
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4/go.mod h1:YqJR8WAAHiKIPesZTr9Cx9Az4fRhRLcJ6GcxzRUZCAc=
github.com/btcsuite/btcwallet/walletdb v1.4.1 h1:NGIGoxx3trpaWqmdOeuhju7KJKp5UM96mQL21idF6RY=
github.com/btcsuite/btcwallet/walletdb v1.4.1/go.mod h1:7ZQ+BvOEre90YT7eSq8bLoxTsgXidUzA/mqbRS114CQ=
github.com/btcsuite/btcwallet/wtxmgr v1.5.1 h1:2yXhMGa4DNz16Mi0e8dVoiFXKOznXlxiGLhB3hKj2uA=
github.com/btcsuite/btcwallet/wtxmgr v1.5.1/go.mod h1:tO4FBSdann0xg/Jtm0grV7t1DzpQMK8nThYVtvSJo/8=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
@ -446,7 +440,6 @@ github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ=
github.com/lightningnetwork/lnd/fn v1.0.2 h1:6u+DHMvpHj09KH2Uw39fsbjydq9JvG23Rc99i+mhI1A=
@ -639,7 +632,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/etcd/api/v3 v3.5.7 h1:sbcmosSVesNrWOJ58ZQFitHMdncusIifYcrBfwrlJSY=

View File

@ -11,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/stretchr/testify/require"
)
type chanFundUtxoSelectionTestCase struct {
@ -131,8 +132,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
localAmt: btcutil.Amount(250_000),
expectedBalance: btcutil.Amount(250_000),
remainingWalletBalance: btcutil.Amount(350_000) -
btcutil.Amount(250_000) -
fundingFee(2, true),
btcutil.Amount(250_000) - fundingFee(2, true),
},
// We are spending the entirety of two selected coins out of
// three available in the wallet and expect no change output and
@ -147,8 +147,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
200_000, 50_000,
},
expectedBalance: btcutil.Amount(200_000) +
btcutil.Amount(50_000) -
fundingFee(2, false),
btcutil.Amount(50_000) - fundingFee(2, false),
remainingWalletBalance: btcutil.Amount(100_000),
},
// Select all coins in wallet and use the maximum available
@ -162,15 +161,14 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
selectedCoins: []btcutil.Amount{200_000, 100_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
reserveAmount -
fundingFee(2, true),
reserveAmount - fundingFee(2, true),
expectedBalance: btcutil.Amount(300_000) -
reserveAmount -
fundingFee(2, true),
reserveAmount - fundingFee(2, true),
remainingWalletBalance: reserveAmount,
},
// Select all coins in wallet towards local amount except for a
// anchor reserve portion.
// Select all coins in wallet towards local amount except for an
// anchor reserve portion. Because the UTXOs are sorted by size
// by default, the reserve amount is just left in the wallet.
{
name: "selected, reserve from selected",
initialCoins: []btcutil.Amount{
@ -181,9 +179,9 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
fundingFee(3, true),
fundingFee(2, true),
expectedBalance: btcutil.Amount(300_000) -
fundingFee(3, true),
fundingFee(2, true),
remainingWalletBalance: reserveAmount,
},
// Select all coins in wallet and use more than the maximum
@ -197,8 +195,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
selectedCoins: []btcutil.Amount{200_000, 100_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
reserveAmount + 1 -
fundingFee(2, true),
reserveAmount + 1 - fundingFee(2, true),
chanOpenShouldFail: true,
expectedErrStr: "reserved wallet balance " +
"invalidated: transaction would leave " +
@ -229,8 +226,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
selectedCoins: []btcutil.Amount{200_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
expectedBalance: btcutil.Amount(200_000) -
reserveAmount -
fundingFee(1, true),
reserveAmount - fundingFee(1, true),
remainingWalletBalance: reserveAmount,
},
// Confirm that already spent outputs can't be reused to fund
@ -249,8 +245,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
success := ht.Run(
tc.name, func(tt *testing.T) {
runUtxoSelectionTestCase(
ht, tt, alice, bob, tc,
reserveAmount,
ht, alice, bob, tc, reserveAmount,
)
},
)
@ -264,7 +259,7 @@ func testChannelUtxoSelection(ht *lntest.HarnessTest) {
// runUtxoSelectionTestCase runs a single test case asserting that test
// conditions are met.
func runUtxoSelectionTestCase(ht *lntest.HarnessTest, t *testing.T, alice,
func runUtxoSelectionTestCase(ht *lntest.HarnessTest, alice,
bob *node.HarnessNode, tc *chanFundUtxoSelectionTestCase,
reserveAmount btcutil.Amount) {
@ -366,4 +361,12 @@ func runUtxoSelectionTestCase(ht *lntest.HarnessTest, t *testing.T, alice,
int64(tc.remainingWalletBalance),
0,
)
// Ensure the anchor channel reserve was carved out.
if commitType == lnrpc.CommitmentType_ANCHORS {
balance := alice.RPC.WalletBalance()
require.EqualValues(
ht, reserveAmount, balance.ReservedBalanceAnchorChan,
)
}
}

View File

@ -3,6 +3,7 @@ package chanfunding
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
@ -11,12 +12,12 @@ import (
type CoinSource interface {
// ListCoins returns all UTXOs from the source that have between
// minConfs and maxConfs number of confirmations.
ListCoins(minConfs, maxConfs int32) ([]Coin, error)
ListCoins(minConfs, maxConfs int32) ([]wallet.Coin, error)
// CoinFromOutPoint attempts to locate details pertaining to a coin
// based on its outpoint. If the coin isn't under the control of the
// backing CoinSource, then an error should be returned.
CoinFromOutPoint(wire.OutPoint) (*Coin, error)
CoinFromOutPoint(wire.OutPoint) (*wallet.Coin, error)
}
// CoinSelectionLocker is an interface that allows the caller to perform an

View File

@ -6,20 +6,20 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// ErrInsufficientFunds is a type matching the error interface which is
// returned when coin selection for a new funding transaction fails to due
// returned when coin selection for a new funding transaction fails due to
// having an insufficient amount of confirmed funds.
type ErrInsufficientFunds struct {
amountAvailable btcutil.Amount
amountSelected btcutil.Amount
}
// Error returns a human readable string describing the error.
// Error returns a human-readable string describing the error.
func (e *ErrInsufficientFunds) Error() string {
return fmt.Sprintf("not enough witness outputs to create funding "+
"transaction, need %v only have %v available",
@ -33,33 +33,33 @@ type errUnsupportedInput struct {
PkScript []byte
}
// Error returns a human readable string describing the error.
// Error returns a human-readable string describing the error.
func (e *errUnsupportedInput) Error() string {
return fmt.Sprintf("unsupported address type: %x", e.PkScript)
}
// Coin represents a spendable UTXO which is available for channel funding.
// This UTXO need not reside in our internal wallet as an example, and instead
// may be derived from an existing watch-only wallet. It wraps both the output
// present within the UTXO set, and also the outpoint that generates this coin.
type Coin struct {
wire.TxOut
wire.OutPoint
}
// selectInputs selects a slice of inputs necessary to meet the specified
// selection amount. If input selection is unable to succeed due to insufficient
// funds, a non-nil error is returned. Additionally, the total amount of the
// selected coins are returned in order for the caller to properly handle
// change+fees.
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
func selectInputs(amt btcutil.Amount, coins []wallet.Coin,
strategy wallet.CoinSelectionStrategy,
feeRate chainfee.SatPerKWeight) (btcutil.Amount, []wallet.Coin, error) {
// All coin selection code in the btcwallet library requires sat/KB.
feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
arrangedCoins, err := strategy.ArrangeCoins(coins, feeSatPerKB)
if err != nil {
return 0, nil, err
}
satSelected := btcutil.Amount(0)
for i, coin := range coins {
for i, coin := range arrangedCoins {
satSelected += btcutil.Amount(coin.Value)
if satSelected >= amt {
return satSelected, coins[:i+1], nil
return satSelected, arrangedCoins[:i+1], nil
}
}
@ -69,8 +69,9 @@ func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, err
// calculateFees returns for the specified utxos and fee rate two fee
// estimates, one calculated using a change output and one without. The weight
// added to the estimator from a change output is for a P2WKH output.
func calculateFees(utxos []Coin, feeRate chainfee.SatPerKWeight) (btcutil.Amount,
btcutil.Amount, error) {
func calculateFees(utxos []wallet.Coin,
feeRate chainfee.SatPerKWeight) (btcutil.Amount, btcutil.Amount,
error) {
var weightEstimate input.TxWeightEstimator
for _, utxo := range utxos {
@ -129,13 +130,17 @@ func sanityCheckFee(totalOut, fee btcutil.Amount) error {
// specified fee rate should be expressed in sat/kw for coin selection to
// function properly.
func CoinSelect(feeRate chainfee.SatPerKWeight, amt, dustLimit btcutil.Amount,
coins []Coin) ([]Coin, btcutil.Amount, error) {
coins []wallet.Coin,
strategy wallet.CoinSelectionStrategy) ([]wallet.Coin, btcutil.Amount,
error) {
amtNeeded := amt
for {
// First perform an initial round of coin selection to estimate
// the required fee.
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
totalSat, selectedUtxos, err := selectInputs(
amtNeeded, coins, strategy, feeRate,
)
if err != nil {
return nil, 0, err
}
@ -198,12 +203,15 @@ func CoinSelect(feeRate chainfee.SatPerKWeight, amt, dustLimit btcutil.Amount,
// amt in total after fees, adhering to the specified fee rate. The selected
// coins, the final output and change values are returned.
func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
dustLimit btcutil.Amount, coins []Coin) ([]Coin, btcutil.Amount,
dustLimit btcutil.Amount, coins []wallet.Coin,
strategy wallet.CoinSelectionStrategy) ([]wallet.Coin, btcutil.Amount,
btcutil.Amount, error) {
// First perform an initial round of coin selection to estimate
// the required fee.
totalSat, selectedUtxos, err := selectInputs(amt, coins)
totalSat, selectedUtxos, err := selectInputs(
amt, coins, strategy, feeRate,
)
if err != nil {
return nil, 0, 0, err
}
@ -259,8 +267,9 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
// available. If insufficient funds are available this method selects all
// available coins.
func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
reserved, dustLimit btcutil.Amount, coins []Coin) ([]Coin,
btcutil.Amount, btcutil.Amount, error) {
reserved, dustLimit btcutil.Amount, coins []wallet.Coin,
strategy wallet.CoinSelectionStrategy) ([]wallet.Coin, btcutil.Amount,
btcutil.Amount, error) {
var (
// selectSubtractFee is tracking if our coin selection was
@ -280,7 +289,7 @@ func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
// First we try to select coins to create an output of the specified
// maxAmount with or without a change output that covers the miner fee.
selected, changeAmt, err := CoinSelect(
feeRate, maxAmount, dustLimit, coins,
feeRate, maxAmount, dustLimit, coins, strategy,
)
var errInsufficientFunds *ErrInsufficientFunds
@ -320,6 +329,7 @@ func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
if selectSubtractFee {
selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
feeRate, totalBalance-reserved, dustLimit, coins,
strategy,
)
if err != nil {
return nil, 0, 0, err
@ -329,7 +339,7 @@ func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
// Sanity check the resulting output values to make sure we don't burn a
// great part to fees.
totalOut := outputAmount + changeAmt
sum := func(coins []Coin) btcutil.Amount {
sum := func(coins []wallet.Coin) btcutil.Amount {
var sum btcutil.Amount
for _, coin := range coins {
sum += btcutil.Amount(coin.Value)

View File

@ -7,6 +7,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
@ -61,7 +62,7 @@ func TestCalculateFees(t *testing.T) {
type testCase struct {
name string
utxos []Coin
utxos []wallet.Coin
expectedFeeNoChange btcutil.Amount
expectedFeeWithChange btcutil.Amount
@ -71,7 +72,7 @@ func TestCalculateFees(t *testing.T) {
testCases := []testCase{
{
name: "one P2WKH input",
utxos: []Coin{
utxos: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -87,7 +88,7 @@ func TestCalculateFees(t *testing.T) {
{
name: "one NP2WKH input",
utxos: []Coin{
utxos: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: np2wkhScript,
@ -103,7 +104,7 @@ func TestCalculateFees(t *testing.T) {
{
name: "not supported P2KH input",
utxos: []Coin{
utxos: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2khScript,
@ -148,7 +149,7 @@ func TestCoinSelect(t *testing.T) {
type testCase struct {
name string
outputValue btcutil.Amount
coins []Coin
coins []wallet.Coin
expectedInput []btcutil.Amount
expectedChange btcutil.Amount
@ -161,7 +162,7 @@ func TestCoinSelect(t *testing.T) {
// This will obviously lead to a change output of
// almost 0.5 BTC.
name: "big change",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -183,7 +184,7 @@ func TestCoinSelect(t *testing.T) {
// This should lead to an error, as we don't have
// enough funds to pay the fee.
name: "nothing left for fees",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -199,7 +200,7 @@ func TestCoinSelect(t *testing.T) {
// as big as possible, such that the remaining change
// would be dust but instead goes to fees.
name: "dust change",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -222,7 +223,7 @@ func TestCoinSelect(t *testing.T) {
// We got just enough funds to create a change output above the
// dust limit.
name: "change right above dustlimit",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -244,7 +245,7 @@ func TestCoinSelect(t *testing.T) {
{
// If more than 20% of funds goes to fees, it should fail.
name: "high fee",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -265,6 +266,7 @@ func TestCoinSelect(t *testing.T) {
selected, changeAmt, err := CoinSelect(
feeRate, test.outputValue, dustLimit, test.coins,
wallet.CoinSelectionLargest,
)
if !test.expectErr && err != nil {
t.Fatalf(err.Error())
@ -323,7 +325,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
name string
highFee bool
spendValue btcutil.Amount
coins []Coin
coins []wallet.Coin
expectedInput []btcutil.Amount
expectedFundingAmt btcutil.Amount
@ -337,7 +339,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// should lead to a funding TX with one output, the
// rest goes to fees.
name: "spend all",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -358,7 +360,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// We have 1.0 BTC available and spend half of it. This
// should lead to a funding TX with a change output.
name: "spend with change",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -379,7 +381,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// The total funds available is below the dust limit
// after paying fees.
name: "dust output",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -397,7 +399,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// is below the dust limit. The remainder should go
// towards the funding output.
name: "dust change",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -416,7 +418,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
{
// We got just enough funds to create an output above the dust limit.
name: "output right above dustlimit",
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -436,7 +438,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// 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: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -456,7 +458,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// If more than 20% of funds goes to fees, it should fail.
name: "high fee",
highFee: true,
coins: []Coin{
coins: []wallet.Coin{
{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
@ -481,6 +483,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
selected, localFundingAmt, changeAmt, err := CoinSelectSubtractFees(
feeRate, test.spendValue, dustLimit, test.coins,
wallet.CoinSelectionLargest,
)
if err != nil {
switch {
@ -551,7 +554,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
minValue btcutil.Amount
maxValue btcutil.Amount
reserved btcutil.Amount
coins []Coin
coins []wallet.Coin
expectedInput []btcutil.Amount
expectedFundingAmt btcutil.Amount
@ -564,7 +567,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// This should lead to a funding TX with one output, the rest
// goes to fees.
name: "spend exactly all",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -582,7 +585,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// This should lead to a funding TX with one output, the rest
// goes to fees.
name: "spend more",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -600,7 +603,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// This should lead to a funding TX with one output and a
// change to subtract the fees from.
name: "spend far below",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -619,7 +622,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// This should lead to a funding TX with one output where the
// fee is subtracted from the total 1 BTC input value.
name: "spend little below",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -638,7 +641,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// The total funds available is below the dust limit after
// paying fees.
name: "dust output",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: int64(
@ -655,7 +658,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// If more than 20% of available wallet funds goes to fees, it
// should fail.
name: "high fee",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: int64(
@ -677,7 +680,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// check could result in a local amount higher than the maximum
// amount that was expected.
name: "sanity check for correct maximum amount",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -695,7 +698,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
// value as change and still maxing out the funding amount.
name: "sanity check for correct reserved amount subtract " +
"from total",
coins: []Coin{{
coins: []wallet.Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
@ -720,6 +723,7 @@ func TestCoinSelectUpToAmount(t *testing.T) {
err := CoinSelectUpToAmount(
feeRate, test.minValue, test.maxValue,
test.reserved, dustLimit, test.coins,
wallet.CoinSelectionLargest,
)
if len(test.expectErr) == 0 && err != nil {
t.Fatalf(err.Error())

View File

@ -31,7 +31,7 @@ type FullIntent struct {
// InputCoins are the set of coins selected as inputs to this funding
// transaction.
InputCoins []Coin
InputCoins []wallet.Coin
// ChangeOutputs are the set of outputs that the Assembler will use as
// change from the main funding transaction.
@ -268,12 +268,12 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
var (
// allCoins refers to the entirety of coins in our
// wallet that are available for funding a channel.
allCoins []Coin
allCoins []wallet.Coin
// manuallySelectedCoins refers to the client-side
// selected coins that should be considered available
// for funding a channel.
manuallySelectedCoins []Coin
manuallySelectedCoins []wallet.Coin
err error
)
@ -309,8 +309,8 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
}
var (
coins []Coin
selectedCoins []Coin
coins []wallet.Coin
selectedCoins []wallet.Coin
localContributionAmt btcutil.Amount
changeAmt btcutil.Amount
)
@ -357,7 +357,9 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// enough funds in the wallet to cover for a reserve.
reserve := r.WalletReserve
if len(manuallySelectedCoins) > 0 {
sumCoins := func(coins []Coin) btcutil.Amount {
sumCoins := func(
coins []wallet.Coin) btcutil.Amount {
var sum btcutil.Amount
for _, coin := range coins {
sum += btcutil.Amount(
@ -390,6 +392,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
err = CoinSelectUpToAmount(
r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt,
reserve, w.cfg.DustLimit, coins,
w.cfg.CoinSelectionStrategy,
)
if err != nil {
return err
@ -423,6 +426,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
selectedCoins, localContributionAmt, changeAmt,
err = CoinSelectSubtractFees(
r.FeeRate, r.LocalAmt, dustLimit, coins,
w.cfg.CoinSelectionStrategy,
)
if err != nil {
return err
@ -435,6 +439,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
localContributionAmt = r.LocalAmt
selectedCoins, changeAmt, err = CoinSelect(
r.FeeRate, r.LocalAmt, dustLimit, coins,
w.cfg.CoinSelectionStrategy,
)
if err != nil {
return err
@ -507,9 +512,10 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// outpointsToCoins maps outpoints to coins in our wallet iff these coins are
// existent and returns an error otherwise.
func outpointsToCoins(outpoints []wire.OutPoint,
coinFromOutPoint func(wire.OutPoint) (*Coin, error)) ([]Coin, error) {
coinFromOutPoint func(wire.OutPoint) (*wallet.Coin, error)) (
[]wallet.Coin, error) {
var selectedCoins []Coin
var selectedCoins []wallet.Coin
for _, outpoint := range outpoints {
coin, err := coinFromOutPoint(
outpoint,

View File

@ -20,6 +20,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/wallet"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
@ -2530,7 +2531,7 @@ func NewCoinSource(w *LightningWallet) *CoinSource {
// ListCoins returns all UTXOs from the source that have between
// minConfs and maxConfs number of confirmations.
func (c *CoinSource) ListCoins(minConfs int32,
maxConfs int32) ([]chanfunding.Coin, error) {
maxConfs int32) ([]wallet.Coin, error) {
utxos, err := c.wallet.ListUnspentWitnessFromDefaultAccount(
minConfs, maxConfs,
@ -2539,9 +2540,9 @@ func (c *CoinSource) ListCoins(minConfs int32,
return nil, err
}
var coins []chanfunding.Coin
var coins []wallet.Coin
for _, utxo := range utxos {
coins = append(coins, chanfunding.Coin{
coins = append(coins, wallet.Coin{
TxOut: wire.TxOut{
Value: int64(utxo.Value),
PkScript: utxo.PkScript,
@ -2556,13 +2557,13 @@ func (c *CoinSource) ListCoins(minConfs int32,
// CoinFromOutPoint attempts to locate details pertaining to a coin based on
// its outpoint. If the coin isn't under the control of the backing CoinSource,
// then an error should be returned.
func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, error) {
func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*wallet.Coin, error) {
inputInfo, err := c.wallet.FetchInputInfo(&op)
if err != nil {
return nil, err
}
return &chanfunding.Coin{
return &wallet.Coin{
TxOut: wire.TxOut{
Value: int64(inputInfo.Value),
PkScript: inputInfo.PkScript,