From 9bdddbcc56e7894fe95b7df9eef9bd99b794d5df Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 6 Feb 2024 12:25:47 +0100 Subject: [PATCH] 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. --- go.mod | 12 ++-- go.sum | 32 ++++------ ...lnd_channel_funding_utxo_selection_test.go | 41 ++++++------ lnwallet/chanfunding/assembler.go | 5 +- lnwallet/chanfunding/coin_select.go | 64 +++++++++++-------- lnwallet/chanfunding/coin_select_test.go | 58 +++++++++-------- lnwallet/chanfunding/wallet_assembler.go | 22 ++++--- lnwallet/wallet.go | 11 ++-- 8 files changed, 131 insertions(+), 114 deletions(-) diff --git a/go.mod b/go.mod index 255362e0f..5546c8440 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1d66ec9da..0d9d0be78 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/itest/lnd_channel_funding_utxo_selection_test.go b/itest/lnd_channel_funding_utxo_selection_test.go index 27c831351..4ba4a557b 100644 --- a/itest/lnd_channel_funding_utxo_selection_test.go +++ b/itest/lnd_channel_funding_utxo_selection_test.go @@ -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, + ) + } } diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go index cd65da8a3..08fe31e43 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/assembler.go @@ -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 diff --git a/lnwallet/chanfunding/coin_select.go b/lnwallet/chanfunding/coin_select.go index 64142cb0a..f4f2e9645 100644 --- a/lnwallet/chanfunding/coin_select.go +++ b/lnwallet/chanfunding/coin_select.go @@ -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) diff --git a/lnwallet/chanfunding/coin_select_test.go b/lnwallet/chanfunding/coin_select_test.go index 96cc44579..b37b027f7 100644 --- a/lnwallet/chanfunding/coin_select_test.go +++ b/lnwallet/chanfunding/coin_select_test.go @@ -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()) diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index c5be0e25c..3c187d808 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -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, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 183f4c72e..12cd3e1d7 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -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,