From 6753a02439d854a43a017470ab79398e2dce771b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 20:55:44 -0700 Subject: [PATCH 01/12] channeldb: add new NoFundingTxBit modifier to ChannelType In this commit, we add a new bit to the existing ChannelType bitfield. If this bit is set, then it signals that we have the funding transaction stored on disk. A future change will enable lnd to have the funding transaction be constructed externally, allowing for things like funding from a hardware wallet, or a channel created as a sub-branch within an existing channel factory. --- channeldb/channel.go | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 9332f50e39..70722dfaa3 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -147,6 +147,11 @@ const ( // type, but it omits the tweak for one's key in the commitment // transaction of the remote party. SingleFunderTweaklessBit ChannelType = 1 << 1 + + // NoFundingTxBit denotes if we have the funding transaction locally on + // disk. This bit may be on if the funding transaction was crafted by a + // wallet external to the primary daemon. + NoFundingTxBit ChannelType = 1 << 2 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -166,6 +171,12 @@ func (c ChannelType) IsTweakless() bool { return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit } +// HasFundingTx returns true if this channel type is one that has a funding +// transaction stored locally. +func (c ChannelType) HasFundingTx() bool { + return c&NoFundingTxBit == 0 +} + // ChannelConstraints represents a set of constraints meant to allow a node to // limit their exposure, enact flow control and ensure that all HTLCs are // economically relevant. This struct will be mirrored for both sides of the @@ -535,7 +546,9 @@ type OpenChannel struct { // is found to be pending. // // NOTE: This value will only be populated for single-funder channels - // for which we are the initiator. + // for which we are the initiator, and that we also have the funding + // transaction for. One can check this by using the HasFundingTx() + // method on the ChanType field. FundingTxn *wire.MsgTx // TODO(roasbeef): eww @@ -2522,6 +2535,16 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error { ) } +// fundingTxPresent returns true if expect the funding transcation to be found +// on disk or already populated within the passed oen chanel struct. +func fundingTxPresent(channel *OpenChannel) bool { + chanType := channel.ChanType + + return chanType.IsSingleFunder() && chanType.HasFundingTx() && + channel.IsInitiator && + !channel.hasChanStatus(ChanStatusRestored) +} + func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error { var w bytes.Buffer if err := WriteElements(&w, @@ -2535,10 +2558,9 @@ func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error { return err } - // For single funder channels that we initiated, write the funding txn. - if channel.ChanType.IsSingleFunder() && channel.IsInitiator && - !channel.hasChanStatus(ChanStatusRestored) { - + // For single funder channels that we initiated, and we have the + // funding transaction, then write the funding txn. + if fundingTxPresent(channel) { if err := WriteElement(&w, channel.FundingTxn); err != nil { return err } @@ -2657,10 +2679,9 @@ func fetchChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error { return err } - // For single funder channels that we initiated, read the funding txn. - if channel.ChanType.IsSingleFunder() && channel.IsInitiator && - !channel.hasChanStatus(ChanStatusRestored) { - + // For single funder channels that we initiated and have the funding + // transaction to, read the funding txn. + if fundingTxPresent(channel) { if err := ReadElement(r, &channel.FundingTxn); err != nil { return err } From f9d22cd90035851c0ef2e75ecc44cc8878fbe124 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 20:56:50 -0700 Subject: [PATCH 02/12] funding: only broadcast the funding transaction if we actually have it --- fundingmanager.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 189eda490f..760687fdd5 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -521,8 +521,9 @@ func (f *fundingManager) start() error { // Rebroadcast the funding transaction for any pending // channel that we initiated. No error will be returned - // if the transaction already has been broadcasted. - if channel.ChanType.IsSingleFunder() && + // if the transaction already has been broadcast. + chanType := channel.ChanType + if chanType.IsSingleFunder() && chanType.HasFundingTx() && channel.IsInitiator { err := f.cfg.PublishTransaction( @@ -1739,21 +1740,28 @@ func (f *fundingManager) handleFundingSigned(fmsg *fundingSignedMsg) { // delete it from our set of active reservations. f.deleteReservationCtx(peerKey, pendingChanID) - // Broadcast the finalized funding transaction to the network. - fundingTx := completeChan.FundingTxn - fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v", - completeChan.FundingOutpoint, spew.Sdump(fundingTx)) + // Broadcast the finalized funding transaction to the network, but only + // if we actually have the funding transaction. + if completeChan.ChanType.HasFundingTx() { + fundingTx := completeChan.FundingTxn - err = f.cfg.PublishTransaction(fundingTx) - if err != nil { - fndgLog.Errorf("Unable to broadcast funding tx for "+ - "ChannelPoint(%v): %v", completeChan.FundingOutpoint, - err) - // We failed to broadcast the funding transaction, but watch - // the channel regardless, in case the transaction made it to - // the network. We will retry broadcast at startup. - // TODO(halseth): retry more often? Handle with CPFP? Just - // delete from the DB? + fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v", + completeChan.FundingOutpoint, spew.Sdump(fundingTx)) + + err = f.cfg.PublishTransaction(fundingTx) + if err != nil { + fndgLog.Errorf("Unable to broadcast funding tx for "+ + "ChannelPoint(%v): %v", + completeChan.FundingOutpoint, err) + + // We failed to broadcast the funding transaction, but + // watch the channel regardless, in case the + // transaction made it to the network. We will retry + // broadcast at startup. + // + // TODO(halseth): retry more often? Handle with CPFP? + // Just delete from the DB? + } } // Now that we have a finalized reservation for this funding flow, From 9eefdef2627089c34c0c80cff894f7646fb67f04 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:14:02 -0700 Subject: [PATCH 03/12] 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. --- lnwallet/chanfunding/coin_select.go | 216 ++++++++++++++++++ .../coin_select_test.go} | 104 ++++++--- lnwallet/wallet.go | 207 ++--------------- 3 files changed, 299 insertions(+), 228 deletions(-) create mode 100644 lnwallet/chanfunding/coin_select.go rename lnwallet/{wallet_test.go => chanfunding/coin_select_test.go} (82%) diff --git a/lnwallet/chanfunding/coin_select.go b/lnwallet/chanfunding/coin_select.go new file mode 100644 index 0000000000..f1ce008d6f --- /dev/null +++ b/lnwallet/chanfunding/coin_select.go @@ -0,0 +1,216 @@ +package chanfunding + +import ( + "fmt" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "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 +// 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. +func (e *ErrInsufficientFunds) Error() string { + return fmt.Sprintf("not enough witness outputs to create funding "+ + "transaction, need %v only have %v available", + e.amountAvailable, e.amountSelected) +} + +// 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) { + satSelected := btcutil.Amount(0) + for i, coin := range coins { + satSelected += btcutil.Amount(coin.Value) + if satSelected >= amt { + return satSelected, coins[:i+1], nil + } + } + + return 0, nil, &ErrInsufficientFunds{amt, satSelected} +} + +// CoinSelect attempts to select a sufficient amount of coins, including a +// 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 +// function properly. +func CoinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount, + coins []Coin) ([]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) + if err != nil { + return nil, 0, err + } + + var weightEstimate input.TxWeightEstimator + + for _, utxo := range selectedUtxos { + switch { + + case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript): + weightEstimate.AddP2WKHInput() + + case txscript.IsPayToScriptHash(utxo.PkScript): + weightEstimate.AddNestedP2WKHInput() + + default: + return nil, 0, fmt.Errorf("unsupported address type: %x", + utxo.PkScript) + } + } + + // Channel funding multisig output is P2WSH. + weightEstimate.AddP2WSHOutput() + + // Assume that change output is a P2WKH output. + // + // TODO: Handle wallets that generate non-witness change + // addresses. + // TODO(halseth): make coinSelect not estimate change output + // for dust change. + weightEstimate.AddP2WKHOutput() + + // The difference between the selected amount and the amount + // requested will be used to pay fees, and generate a change + // output with the remaining. + overShootAmt := totalSat - amt + + // Based on the estimated size and fee rate, if the excess + // amount isn't enough to pay fees, then increase the requested + // coin amount by the estimate required fee, performing another + // round of coin selection. + totalWeight := int64(weightEstimate.Weight()) + requiredFee := feeRate.FeeForWeight(totalWeight) + if overShootAmt < requiredFee { + amtNeeded = amt + requiredFee + continue + } + + // If the fee is sufficient, then calculate the size of the + // change output. + changeAmt := overShootAmt - requiredFee + + return selectedUtxos, changeAmt, nil + } +} + +// CoinSelectSubtractFees attempts to select coins such that we'll spend up to +// 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, + btcutil.Amount, error) { + + // First perform an initial round of coin selection to estimate + // the required fee. + totalSat, selectedUtxos, err := selectInputs(amt, coins) + if err != nil { + return nil, 0, 0, err + } + + var weightEstimate input.TxWeightEstimator + for _, utxo := range selectedUtxos { + switch { + + case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript): + weightEstimate.AddP2WKHInput() + + case txscript.IsPayToScriptHash(utxo.PkScript): + weightEstimate.AddNestedP2WKHInput() + + default: + return nil, 0, 0, fmt.Errorf("unsupported address "+ + "type: %x", utxo.PkScript) + } + } + + // Channel funding multisig output is P2WSH. + weightEstimate.AddP2WSHOutput() + + // At this point we've got two possibilities, either create a + // change output, or not. We'll first try without creating a + // change output. + // + // Estimate the fee required for a transaction without a change + // output. + totalWeight := int64(weightEstimate.Weight()) + requiredFee := feeRate.FeeForWeight(totalWeight) + + // For a transaction without a change output, we'll let everything go + // to our multi-sig output after subtracting fees. + outputAmt := totalSat - requiredFee + changeAmt := btcutil.Amount(0) + + // If the the output is too small after subtracting the fee, the coin + // selection cannot be performed with an amount this small. + if outputAmt <= dustLimit { + return nil, 0, 0, fmt.Errorf("output amount(%v) after "+ + "subtracting fees(%v) below dust limit(%v)", outputAmt, + requiredFee, dustLimit) + } + + // We were able to create a transaction with no change from the + // selected inputs. We'll remember the resulting values for + // now, while we try to add a change output. Assume that change output + // is a P2WKH output. + weightEstimate.AddP2WKHOutput() + + // Now that we have added the change output, redo the fee + // estimate. + totalWeight = int64(weightEstimate.Weight()) + requiredFee = feeRate.FeeForWeight(totalWeight) + + // For a transaction with a change output, everything we don't spend + // will go to change. + newChange := totalSat - amt + newOutput := amt - requiredFee + + // If adding a change output leads to both outputs being above + // the dust limit, we'll add the change output. Otherwise we'll + // go with the no change tx we originally found. + if newChange > dustLimit && newOutput > dustLimit { + outputAmt = newOutput + changeAmt = newChange + } + + // Sanity check the resulting output values to make sure we + // don't burn a great part to fees. + totalOut := outputAmt + changeAmt + fee := totalSat - totalOut + + // Fail if more than 20% goes to fees. + // TODO(halseth): smarter fee limit. Make configurable or dynamic wrt + // total funding size? + if fee > totalOut/5 { + return nil, 0, 0, fmt.Errorf("fee %v on total output"+ + "value %v", fee, totalOut) + } + + return selectedUtxos, outputAmt, changeAmt, nil +} diff --git a/lnwallet/wallet_test.go b/lnwallet/chanfunding/coin_select_test.go similarity index 82% rename from lnwallet/wallet_test.go rename to lnwallet/chanfunding/coin_select_test.go index 903f5e4d9c..67b2497680 100644 --- a/lnwallet/wallet_test.go +++ b/lnwallet/chanfunding/coin_select_test.go @@ -1,13 +1,21 @@ -package lnwallet +package chanfunding import ( + "encoding/hex" "testing" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +var ( + p2wkhScript, _ = hex.DecodeString( + "001411034bdcb6ccb7744fdfdeea958a6fb0b415a032", + ) +) + // 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. @@ -48,7 +56,7 @@ func TestCoinSelect(t *testing.T) { type testCase struct { name string outputValue btcutil.Amount - coins []*Utxo + coins []Coin expectedInput []btcutil.Amount expectedChange btcutil.Amount @@ -61,10 +69,12 @@ func TestCoinSelect(t *testing.T) { // This will obviously lead to a change output of // almost 0.5 BTC. name: "big change", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, outputValue: 0.5 * btcutil.SatoshiPerBitcoin, @@ -81,10 +91,12 @@ 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: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, outputValue: 1 * btcutil.SatoshiPerBitcoin, @@ -95,10 +107,12 @@ func TestCoinSelect(t *testing.T) { // as big as possible, such that the remaining change // will be dust. name: "dust change", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, // We tune the output value by subtracting the expected @@ -117,10 +131,12 @@ func TestCoinSelect(t *testing.T) { // as big as possible, such that there is nothing left // for change. name: "no change", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, // We tune the output value to be the maximum amount @@ -143,7 +159,7 @@ func TestCoinSelect(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - selected, changeAmt, err := coinSelect( + selected, changeAmt, err := CoinSelect( feeRate, test.outputValue, test.coins, ) if !test.expectErr && err != nil { @@ -166,7 +182,7 @@ func TestCoinSelect(t *testing.T) { } for i, coin := range selected { - if coin.Value != test.expectedInput[i] { + if coin.Value != int64(test.expectedInput[i]) { t.Fatalf("expected input %v to have value %v, "+ "had %v", i, test.expectedInput[i], coin.Value) @@ -195,7 +211,7 @@ func TestCoinSelectSubtractFees(t *testing.T) { type testCase struct { name string spendValue btcutil.Amount - coins []*Utxo + coins []Coin expectedInput []btcutil.Amount expectedFundingAmt btcutil.Amount @@ -209,10 +225,12 @@ func TestCoinSelectSubtractFees(t *testing.T) { // should lead to a funding TX with one output, the // rest goes to fees. name: "spend all", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, spendValue: 1 * btcutil.SatoshiPerBitcoin, @@ -228,10 +246,12 @@ func TestCoinSelectSubtractFees(t *testing.T) { // The total funds available is below the dust limit // after paying fees. name: "dust output", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: fundingFee(feeRate, 1, false) + dust, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: int64(fundingFee(feeRate, 1, false) + dust), + }, }, }, spendValue: fundingFee(feeRate, 1, false) + dust, @@ -243,10 +263,12 @@ func TestCoinSelectSubtractFees(t *testing.T) { // is below the dust limit. The remainder should go // towards the funding output. name: "dust change", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 1 * btcutil.SatoshiPerBitcoin, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: 1 * btcutil.SatoshiPerBitcoin, + }, }, }, spendValue: 1*btcutil.SatoshiPerBitcoin - dust, @@ -260,10 +282,12 @@ func TestCoinSelectSubtractFees(t *testing.T) { { // We got just enough funds to create an output above the dust limit. name: "output right above dustlimit", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: fundingFee(feeRate, 1, false) + dustLimit + 1, + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: int64(fundingFee(feeRate, 1, false) + dustLimit + 1), + }, }, }, spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1, @@ -278,10 +302,12 @@ 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: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: fundingFee(feeRate, 1, false) + 2*(dustLimit+1), + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: int64(fundingFee(feeRate, 1, false) + 2*(dustLimit+1)), + }, }, }, spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1, @@ -295,10 +321,12 @@ func TestCoinSelectSubtractFees(t *testing.T) { { // If more than 20% of funds goes to fees, it should fail. name: "high fee", - coins: []*Utxo{ + coins: []Coin{ { - AddressType: WitnessPubKey, - Value: 5 * fundingFee(feeRate, 1, false), + TxOut: wire.TxOut{ + PkScript: p2wkhScript, + Value: int64(5 * fundingFee(feeRate, 1, false)), + }, }, }, spendValue: 5 * fundingFee(feeRate, 1, false), @@ -308,8 +336,10 @@ func TestCoinSelectSubtractFees(t *testing.T) { } for _, test := range testCases { + test := test + t.Run(test.name, func(t *testing.T) { - selected, localFundingAmt, changeAmt, err := coinSelectSubtractFees( + selected, localFundingAmt, changeAmt, err := CoinSelectSubtractFees( feeRate, test.spendValue, dustLimit, test.coins, ) if !test.expectErr && err != nil { @@ -332,7 +362,7 @@ func TestCoinSelectSubtractFees(t *testing.T) { } for i, coin := range selected { - if coin.Value != test.expectedInput[i] { + if coin.Value != int64(test.expectedInput[i]) { t.Fatalf("expected input %v to have value %v, "+ "had %v", i, test.expectedInput[i], coin.Value) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 342579e901..b6b552a5f5 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -22,6 +22,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwallet/chanvalidate" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" @@ -33,20 +34,6 @@ const ( msgBufferSize = 100 ) -// ErrInsufficientFunds is a type matching the error interface which is -// returned when coin selection for a new funding transaction fails to due -// having an insufficient amount of confirmed funds. -type ErrInsufficientFunds struct { - amountAvailable btcutil.Amount - amountSelected btcutil.Amount -} - -func (e *ErrInsufficientFunds) Error() string { - return fmt.Sprintf("not enough witness outputs to create funding transaction,"+ - " need %v only have %v available", e.amountAvailable, - e.amountSelected) -} - // InitFundingReserveMsg is the first message sent to initiate the workflow // required to open a payment channel with a remote peer. The initial required // parameters are configurable across channels. These parameters are to be @@ -1341,13 +1328,24 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight, // Find all unlocked unspent witness outputs that satisfy the minimum // number of confirmations required. - coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32) + utxos, err := l.ListUnspentWitness(minConfs, math.MaxInt32) if err != nil { return nil, err } + coins := make([]chanfunding.Coin, len(utxos), 0) + for _, utxo := range utxos { + coins = append(coins, chanfunding.Coin{ + TxOut: wire.TxOut{ + Value: int64(utxo.Value), + PkScript: utxo.PkScript, + }, + OutPoint: utxo.OutPoint, + }) + } + var ( - selectedCoins []*Utxo + selectedCoins []chanfunding.Coin fundingAmt btcutil.Amount changeAmt btcutil.Amount ) @@ -1361,7 +1359,7 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight, // won't deduct more that the specified balance from our wallet. case subtractFees: dustLimit := l.Cfg.DefaultConstraints.DustLimit - selectedCoins, fundingAmt, changeAmt, err = coinSelectSubtractFees( + selectedCoins, fundingAmt, changeAmt, err = chanfunding.CoinSelectSubtractFees( feeRate, amt, dustLimit, coins, ) if err != nil { @@ -1372,7 +1370,7 @@ func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight, // amount. default: fundingAmt = amt - selectedCoins, changeAmt, err = coinSelect(feeRate, amt, coins) + selectedCoins, changeAmt, err = chanfunding.CoinSelect(feeRate, amt, coins) if err != nil { return nil, err } @@ -1468,179 +1466,6 @@ func initStateHints(commit1, commit2 *wire.MsgTx, return nil } -// 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 []*Utxo) (btcutil.Amount, []*Utxo, error) { - satSelected := btcutil.Amount(0) - for i, coin := range coins { - satSelected += coin.Value - if satSelected >= amt { - return satSelected, coins[:i+1], nil - } - } - return 0, nil, &ErrInsufficientFunds{amt, satSelected} -} - -// coinSelect attempts to select a sufficient amount of coins, including a -// 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 -// function properly. -func coinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount, - coins []*Utxo) ([]*Utxo, 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) - if err != nil { - return nil, 0, err - } - - var weightEstimate input.TxWeightEstimator - - for _, utxo := range selectedUtxos { - switch utxo.AddressType { - case WitnessPubKey: - weightEstimate.AddP2WKHInput() - case NestedWitnessPubKey: - weightEstimate.AddNestedP2WKHInput() - default: - return nil, 0, fmt.Errorf("unsupported address type: %v", - utxo.AddressType) - } - } - - // Channel funding multisig output is P2WSH. - weightEstimate.AddP2WSHOutput() - - // Assume that change output is a P2WKH output. - // - // TODO: Handle wallets that generate non-witness change - // addresses. - // TODO(halseth): make coinSelect not estimate change output - // for dust change. - weightEstimate.AddP2WKHOutput() - - // The difference between the selected amount and the amount - // requested will be used to pay fees, and generate a change - // output with the remaining. - overShootAmt := totalSat - amt - - // Based on the estimated size and fee rate, if the excess - // amount isn't enough to pay fees, then increase the requested - // coin amount by the estimate required fee, performing another - // round of coin selection. - totalWeight := int64(weightEstimate.Weight()) - requiredFee := feeRate.FeeForWeight(totalWeight) - if overShootAmt < requiredFee { - amtNeeded = amt + requiredFee - continue - } - - // If the fee is sufficient, then calculate the size of the - // change output. - changeAmt := overShootAmt - requiredFee - - return selectedUtxos, changeAmt, nil - } -} - -// coinSelectSubtractFees attempts to select coins such that we'll spend up to -// 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 []*Utxo) ([]*Utxo, btcutil.Amount, - btcutil.Amount, error) { - - // First perform an initial round of coin selection to estimate - // the required fee. - totalSat, selectedUtxos, err := selectInputs(amt, coins) - if err != nil { - return nil, 0, 0, err - } - - var weightEstimate input.TxWeightEstimator - for _, utxo := range selectedUtxos { - switch utxo.AddressType { - case WitnessPubKey: - weightEstimate.AddP2WKHInput() - case NestedWitnessPubKey: - weightEstimate.AddNestedP2WKHInput() - default: - return nil, 0, 0, fmt.Errorf("unsupported "+ - "address type: %v", utxo.AddressType) - } - } - - // Channel funding multisig output is P2WSH. - weightEstimate.AddP2WSHOutput() - - // At this point we've got two possibilities, either create a - // change output, or not. We'll first try without creating a - // change output. - // - // Estimate the fee required for a transaction without a change - // output. - totalWeight := int64(weightEstimate.Weight()) - requiredFee := feeRate.FeeForWeight(totalWeight) - - // For a transaction without a change output, we'll let everything go - // to our multi-sig output after subtracting fees. - outputAmt := totalSat - requiredFee - changeAmt := btcutil.Amount(0) - - // If the the output is too small after subtracting the fee, the coin - // selection cannot be performed with an amount this small. - if outputAmt <= dustLimit { - return nil, 0, 0, fmt.Errorf("output amount(%v) after "+ - "subtracting fees(%v) below dust limit(%v)", outputAmt, - requiredFee, dustLimit) - } - - // We were able to create a transaction with no change from the - // selected inputs. We'll remember the resulting values for - // now, while we try to add a change output. Assume that change output - // is a P2WKH output. - weightEstimate.AddP2WKHOutput() - - // Now that we have added the change output, redo the fee - // estimate. - totalWeight = int64(weightEstimate.Weight()) - requiredFee = feeRate.FeeForWeight(totalWeight) - - // For a transaction with a change output, everything we don't spend - // will go to change. - newChange := totalSat - amt - newOutput := amt - requiredFee - - // If adding a change output leads to both outputs being above - // the dust limit, we'll add the change output. Otherwise we'll - // go with the no change tx we originally found. - if newChange > dustLimit && newOutput > dustLimit { - outputAmt = newOutput - changeAmt = newChange - } - - // Sanity check the resulting output values to make sure we - // don't burn a great part to fees. - totalOut := outputAmt + changeAmt - fee := totalSat - totalOut - - // Fail if more than 20% goes to fees. - // TODO(halseth): smarter fee limit. Make configurable or dynamic wrt - // total funding size? - if fee > totalOut/5 { - return nil, 0, 0, fmt.Errorf("fee %v on total output"+ - "value %v", fee, totalOut) - } - - return selectedUtxos, outputAmt, changeAmt, nil -} - // ValidateChannel will attempt to fully validate a newly mined channel, given // its funding transaction and existing channel state. If this method returns // an error, then the mined channel is invalid, and shouldn't be used. From 4e955dfac6f4f745b09bfa61a812722bd8a760d9 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:29:37 -0700 Subject: [PATCH 04/12] lnwallet: remove unused openChanDetails struct We also remove some related and also unused attributes as well along the way. --- lnwallet/reservation.go | 9 --------- lnwallet/wallet.go | 7 ------- 2 files changed, 16 deletions(-) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 36574bb598..e1231e4526 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -114,13 +114,6 @@ type ChannelReservation struct { // commitment state. pushMSat lnwire.MilliSatoshi - // chanOpen houses a struct containing the channel and additional - // confirmation details will be sent on once the channel is considered - // 'open'. A channel is open once the funding transaction has reached a - // sufficient number of confirmations. - chanOpen chan *openChanDetails - chanOpenErr chan error - wallet *LightningWallet } @@ -259,8 +252,6 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, }, pushMSat: pushMSat, reservationID: id, - chanOpen: make(chan *openChanDetails, 1), - chanOpenErr: make(chan error, 1), wallet: wallet, }, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index b6b552a5f5..f5a44f5c39 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -949,13 +949,6 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg return } -// openChanDetails contains a "finalized" channel which can be considered -// "open" according to the requested confirmation depth at reservation -// initialization. Additionally, the struct contains additional details -// pertaining to the exact location in the main chain in-which the transaction -// was confirmed. -type openChanDetails struct { -} // handleFundingCounterPartySigs is the final step in the channel reservation // workflow. During this step, we validate *all* the received signatures for From d422ebbc6683dd8c40d18ad179df683259b04268 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:32:04 -0700 Subject: [PATCH 05/12] lnwallet/chanfunding: introduce new channel funding abstractions In this commit, we introduce a series of new abstractions for channel funding. The end goal is to enable uses cases that construct the funding transaction externally, eventually handing the funding outpoint to lnd. An example of such a use case includes channel factories and external channel funding using a hardware wallet. We also add a new chanfunding.Assembler meant to allow external channel funding in contexts similar to how channel factories can be constructed. With this channel funder, we'll only obtain the channel point and funding output from it, as this alone is enough to carry out a funding flow as normal. --- lnwallet/chanfunding/assembler.go | 137 +++++++++ lnwallet/chanfunding/canned_assembler.go | 187 ++++++++++++ lnwallet/chanfunding/log.go | 29 ++ lnwallet/chanfunding/wallet_assembler.go | 343 +++++++++++++++++++++++ log.go | 2 + 5 files changed, 698 insertions(+) create mode 100644 lnwallet/chanfunding/assembler.go create mode 100644 lnwallet/chanfunding/canned_assembler.go create mode 100644 lnwallet/chanfunding/log.go create mode 100644 lnwallet/chanfunding/wallet_assembler.go diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go new file mode 100644 index 0000000000..e320d9eebc --- /dev/null +++ b/lnwallet/chanfunding/assembler.go @@ -0,0 +1,137 @@ +package chanfunding + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +// CoinSource is an interface that allows a caller to access a source of UTXOs +// to use when attempting to fund a new channel. +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) + + // 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) +} + +// CoinSelectionLocker is an interface that allows the caller to perform an +// operation, which is synchronized with all coin selection attempts. This can +// be used when an operation requires that all coin selection operations cease +// forward progress. Think of this as an exclusive lock on coin selection +// operations. +type CoinSelectionLocker interface { + // WithCoinSelectLock will execute the passed function closure in a + // synchronized manner preventing any coin selection operations from + // proceeding while the closure if executing. This can be seen as the + // ability to execute a function closure under an exclusive coin + // selection lock. + WithCoinSelectLock(func() error) error +} + +// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the +// outpoints shouldn't be used for any sort of channel funding of coin +// selection. Locked outpoints are not expected to be persisted between +// restarts. +type OutpointLocker interface { + // LockOutpoint locks a target outpoint, rendering it unusable for coin + // selection. + LockOutpoint(o wire.OutPoint) + + // UnlockOutpoint unlocks a target outpoint, allowing it to be used for + // coin selection once again. + UnlockOutpoint(o wire.OutPoint) +} + +// Request is a new request for funding a channel. The items in the struct +// governs how the final channel point will be provisioned by the target +// Assembler. +type Request struct { + // LocalAmt is the amount of coins we're placing into the funding + // output. + LocalAmt btcutil.Amount + + // RemoteAmt is the amount of coins the remote party is contributing to + // the funding output. + RemoteAmt btcutil.Amount + + // MinConfs controls how many confirmations a coin need to be eligible + // to be used as an input to the funding transaction. If this value is + // set to zero, then zero conf outputs may be spent. + MinConfs int32 + + // SubtractFees should be set if we intend to spend exactly LocalAmt + // when opening the channel, subtracting the fees from the funding + // output. This can be used for instance to use all our remaining funds + // to open the channel, since it will take fees into + // account. + SubtractFees bool + + // FeeRate is the fee rate in sat/kw that the funding transaction + // should carry. + FeeRate chainfee.SatPerKWeight + + // ChangeAddr is a closure that will provide the Assembler with a + // change address for the funding transaction if needed. + ChangeAddr func() (btcutil.Address, error) +} + +// Intent is returned by an Assembler and represents the base functionality the +// caller needs to proceed with channel funding on a higher level. If the +// Cancel method is called, then all resources assembled to fund the channel +// will be released back to the eligible pool. +type Intent interface { + // FundingOutput returns the witness script, and the output that + // creates the funding output. + FundingOutput() ([]byte, *wire.TxOut, error) + + // ChanPoint returns the final outpoint that will create the funding + // output described above. + ChanPoint() (*wire.OutPoint, error) + + // RemoteFundingAmt is the amount the remote party put into the + // channel. + RemoteFundingAmt() btcutil.Amount + + // LocalFundingAmt is the amount we put into the channel. This may + // differ from the local amount requested, as depending on coin + // selection, we may bleed from of that LocalAmt into fees to minimize + // change. + LocalFundingAmt() btcutil.Amount + + // Cancel allows the caller to cancel a funding Intent at any time. + // This will return any resources such as coins back to the eligible + // pool to be used in order channel fundings. + Cancel() +} + +// Assembler is an abstract object that is capable of assembling everything +// needed to create a new funding output. As an example, this assembler may be +// our core backing wallet, an interactive PSBT based assembler, an assembler +// than can aggregate multiple intents into a single funding transaction, or an +// external protocol that creates a funding output out-of-band such as channel +// factories. +type Assembler interface { + // ProvisionChannel returns a populated Intent that can be used to + // further the channel funding workflow. Depending on the + // implementation of Assembler, additional state machine (Intent) + // actions may be required before the FundingOutput and ChanPoint are + // made available to the caller. + ProvisionChannel(*Request) (Intent, error) +} + +// FundingTxAssembler is a super-set of the regular Assembler interface that's +// also able to provide a fully populated funding transaction via the intents +// that it produuces. +type FundingTxAssembler interface { + Assembler + + // FundingTxAvailable is an empty method that an assembler can + // implement to signal to callers that its able to provide the funding + // transaction for the channel via the intent it returns. + FundingTxAvailable() +} diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go new file mode 100644 index 0000000000..10fe811b8e --- /dev/null +++ b/lnwallet/chanfunding/canned_assembler.go @@ -0,0 +1,187 @@ +package chanfunding + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +// ShimIntent is an intent created by the CannedAssembler which represents a +// funding output to be created that was constructed outside the wallet. This +// might be used when a hardware wallet, or a channel factory is the entity +// crafting the funding transaction, and not lnd. +type ShimIntent struct { + // localFundingAmt is the final amount we put into the funding output. + localFundingAmt btcutil.Amount + + // remoteFundingAmt is the final amount the remote party put into the + // funding output. + remoteFundingAmt btcutil.Amount + + // localKey is our multi-sig key. + localKey *keychain.KeyDescriptor + + // remoteKey is the remote party's multi-sig key. + remoteKey *btcec.PublicKey + + // chanPoint is the final channel point for the to be created channel. + chanPoint *wire.OutPoint +} + +// FundingOutput returns the witness script, and the output that creates the +// funding output. +// +// NOTE: This method satisfies the chanfunding.Intent interface. +func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { + if s.localKey == nil || s.remoteKey == nil { + return nil, nil, fmt.Errorf("unable to create witness " + + "script, no funding keys") + } + + totalAmt := s.localFundingAmt + s.remoteFundingAmt + return input.GenFundingPkScript( + s.localKey.PubKey.SerializeCompressed(), + s.remoteKey.SerializeCompressed(), + int64(totalAmt), + ) +} + +// Cancel allows the caller to cancel a funding Intent at any time. This will +// return any resources such as coins back to the eligible pool to be used in +// order channel fundings. +// +// NOTE: This method satisfies the chanfunding.Intent interface. +func (s *ShimIntent) Cancel() { +} + +// RemoteFundingAmt is the amount the remote party put into the channel. +// +// NOTE: This method satisfies the chanfunding.Intent interface. +func (s *ShimIntent) LocalFundingAmt() btcutil.Amount { + return s.localFundingAmt +} + +// LocalFundingAmt is the amount we put into the channel. This may differ from +// the local amount requested, as depending on coin selection, we may bleed +// from of that LocalAmt into fees to minimize change. +// +// NOTE: This method satisfies the chanfunding.Intent interface. +func (s *ShimIntent) RemoteFundingAmt() btcutil.Amount { + return s.remoteFundingAmt +} + +// ChanPoint returns the final outpoint that will create the funding output +// described above. +// +// NOTE: This method satisfies the chanfunding.Intent interface. +func (s *ShimIntent) ChanPoint() (*wire.OutPoint, error) { + if s.chanPoint == nil { + return nil, fmt.Errorf("chan point unknown, funding output " + + "not constructed") + } + + return s.chanPoint, nil +} + +// FundingKeys couples our multi-sig key along with the remote party's key. +type FundingKeys struct { + // LocalKey is our multi-sig key. + LocalKey *keychain.KeyDescriptor + + // RemoteKey is the multi-sig key of the remote party. + RemoteKey *btcec.PublicKey +} + +// MultiSigKeys returns the committed multi-sig keys, but only if they've been +// specified/provided. +func (s *ShimIntent) MultiSigKeys() (*FundingKeys, error) { + if s.localKey == nil || s.remoteKey == nil { + return nil, fmt.Errorf("unknown funding keys") + } + + return &FundingKeys{ + LocalKey: s.localKey, + RemoteKey: s.remoteKey, + }, nil +} + +// A compile-time check to ensure ShimIntent adheres to the Intent interface. +var _ Intent = (*ShimIntent)(nil) + +// CannedAssembler is a type of chanfunding.Assembler wherein the funding +// transaction is constructed outside of lnd, and may already exist. This +// Assembler serves as a shim which gives the funding flow the only thing it +// actually needs to proceed: the channel point. +type CannedAssembler struct { + // fundingAmt is the total amount of coins in the funding output. + fundingAmt btcutil.Amount + + // localKey is our multi-sig key. + localKey *keychain.KeyDescriptor + + // remoteKey is the remote party's multi-sig key. + remoteKey *btcec.PublicKey + + // chanPoint is the final channel point for the to be created channel. + chanPoint wire.OutPoint + + // initiator indicates if we're the initiator or the channel or not. + initiator bool +} + +// NewCannedAssembler creates a new CannedAssembler from the material required +// to construct a funding output and channel point. +func NewCannedAssembler(chanPoint wire.OutPoint, fundingAmt btcutil.Amount, + localKey *keychain.KeyDescriptor, + remoteKey *btcec.PublicKey, initiator bool) *CannedAssembler { + + return &CannedAssembler{ + initiator: initiator, + localKey: localKey, + remoteKey: remoteKey, + fundingAmt: fundingAmt, + chanPoint: chanPoint, + } +} + +// ProvisionChannel creates a new ShimIntent given the passed funding Request. +// The returned intent is immediately able to provide the channel point and +// funding output as they've already been created outside lnd. +// +// NOTE: This method satisfies the chanfunding.Assembler interface. +func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) { + switch { + // A simple sanity check to ensure the provision request matches the + // re-made shim intent. + case req.LocalAmt != c.fundingAmt: + return nil, fmt.Errorf("intent doesn't match canned assembler") + + // We'll exit out if this field is set as the funding transaction has + // already been assembled, so we don't influence coin selection.. + case req.SubtractFees: + return nil, fmt.Errorf("SubtractFees ignored, funding " + + "transaction is frozen") + } + + intent := &ShimIntent{ + localKey: c.localKey, + remoteKey: c.remoteKey, + chanPoint: &c.chanPoint, + } + + if c.initiator { + intent.localFundingAmt = c.fundingAmt + } else { + intent.remoteFundingAmt = c.fundingAmt + } + + return intent, nil +} + +// A compile-time assertion to ensure CannedAssembler meets the Assembler +// interface. +var _ Assembler = (*CannedAssembler)(nil) diff --git a/lnwallet/chanfunding/log.go b/lnwallet/chanfunding/log.go new file mode 100644 index 0000000000..159a96ca1d --- /dev/null +++ b/lnwallet/chanfunding/log.go @@ -0,0 +1,29 @@ +package chanfunding + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("CHFD", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go new file mode 100644 index 0000000000..554654feae --- /dev/null +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -0,0 +1,343 @@ +package chanfunding + +import ( + "math" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/txsort" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" +) + +// FullIntent is an intent that is fully backed by the internal wallet. This +// intent differs from the ShimIntent, in that the funding transaction will be +// constructed internally, and will consist of only inputs we wholly control. +// This Intent implements a basic state machine that must be executed in order +// before CompileFundingTx can be called. +// +// Steps to final channel provisioning: +// 1. Call BindKeys to notify the intent which keys to use when constructing +// the multi-sig output. +// 2. Call CompileFundingTx afterwards to obtain the funding transaction. +// +// If either of these steps fail, then the Cancel method MUST be called. +type FullIntent struct { + ShimIntent + + // InputCoins are the set of coins selected as inputs to this funding + // transaction. + InputCoins []Coin + + // ChangeOutputs are the set of outputs that the Assembler will use as + // change from the main funding transaction. + ChangeOutputs []*wire.TxOut + + // coinLocker is the Assembler's instance of the OutpointLocker + // interface. + coinLocker OutpointLocker + + // coinSource is the Assembler's instance of the CoinSource interface. + coinSource CoinSource + + // signer is the Assembler's instance of the Singer interface. + signer input.Signer +} + +// BindKeys is a method unique to the FullIntent variant. This allows the +// caller to decide precisely which keys are used in the final funding +// transaction. This is kept out of the main Assembler as these may may not +// necessarily be under full control of the wallet. Only after this method has +// been executed will CompileFundingTx succeed. +func (f *FullIntent) BindKeys(localKey *keychain.KeyDescriptor, + remoteKey *btcec.PublicKey) { + + f.localKey = localKey + f.remoteKey = remoteKey +} + +// CompileFundingTx is to be called after BindKeys on the sub-intent has been +// called. This method will construct the final funding transaction, and fully +// sign all inputs that are known by the backing CoinSource. After this method +// returns, the Intent is assumed to be complete, as the output can be created +// at any point. +func (f *FullIntent) CompileFundingTx(extraInputs []*wire.TxIn, + extraOutputs []*wire.TxOut) (*wire.MsgTx, error) { + + // Create a blank, fresh transaction. Soon to be a complete funding + // transaction which will allow opening a lightning channel. + fundingTx := wire.NewMsgTx(2) + + // Add all multi-party inputs and outputs to the transaction. + for _, coin := range f.InputCoins { + fundingTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: coin.OutPoint, + }) + } + for _, theirInput := range extraInputs { + fundingTx.AddTxIn(theirInput) + } + for _, ourChangeOutput := range f.ChangeOutputs { + fundingTx.AddTxOut(ourChangeOutput) + } + for _, theirChangeOutput := range extraOutputs { + fundingTx.AddTxOut(theirChangeOutput) + } + + _, fundingOutput, err := f.FundingOutput() + if err != nil { + return nil, err + } + + // Sort the transaction. Since both side agree to a canonical ordering, + // by sorting we no longer need to send the entire transaction. Only + // signatures will be exchanged. + fundingTx.AddTxOut(fundingOutput) + txsort.InPlaceSort(fundingTx) + + // Now that the funding tx has been fully assembled, we'll locate the + // index of the funding output so we can create our final channel + // point. + _, multiSigIndex := input.FindScriptOutputIndex( + fundingTx, fundingOutput.PkScript, + ) + + // Next, sign all inputs that are ours, collecting the signatures in + // order of the inputs. + signDesc := input.SignDescriptor{ + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(fundingTx), + } + for i, txIn := range fundingTx.TxIn { + // We can only sign this input if it's ours, so we'll ask the + // coin source if it can map this outpoint into a coin we own. + // If not, then we'll continue as it isn't our input. + info, err := f.coinSource.CoinFromOutPoint( + txIn.PreviousOutPoint, + ) + if err != nil { + continue + } + + // Now that we know the input is ours, we'll populate the + // signDesc with the per input unique information. + signDesc.Output = &wire.TxOut{ + Value: info.Value, + PkScript: info.PkScript, + } + signDesc.InputIndex = i + + // Finally, we'll sign the input as is, and populate the input + // with the witness and sigScript (if needed). + inputScript, err := f.signer.ComputeInputScript( + fundingTx, &signDesc, + ) + if err != nil { + return nil, err + } + + txIn.SignatureScript = inputScript.SigScript + txIn.Witness = inputScript.Witness + } + + // Finally, we'll populate the chanPoint now that we've fully + // constructed the funding transaction. + f.chanPoint = &wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: multiSigIndex, + } + + return fundingTx, nil +} + +// Cancel allows the caller to cancel a funding Intent at any time. This will +// return any resources such as coins back to the eligible pool to be used in +// order channel fundings. +// +// NOTE: Part of the chanfunding.Intent interface. +func (f *FullIntent) Cancel() { + for _, coin := range f.InputCoins { + f.coinLocker.UnlockOutpoint(coin.OutPoint) + } + + f.ShimIntent.Cancel() +} + +// A compile-time check to ensure FullIntent meets the Intent interface. +var _ Intent = (*FullIntent)(nil) + +// WalletConfig is the main config of the WalletAssembler. +type WalletConfig struct { + // CoinSource is what the WalletAssembler uses to list/locate coins. + CoinSource CoinSource + + // CoinSelectionLocker allows the WalletAssembler to gain exclusive + // access to the current set of coins returned by the CoinSource. + CoinSelectLocker CoinSelectionLocker + + // CoinLocker is what the WalletAssembler uses to lock coins that may + // be used as inputs for a new funding transaction. + CoinLocker OutpointLocker + + // Signer allows the WalletAssembler to sign inputs on any potential + // funding transactions. + Signer input.Signer + + // DustLimit is the current dust limit. We'll use this to ensure that + // we don't make dust outputs on the funding transaction. + DustLimit btcutil.Amount +} + +// WalletAssembler is an instance of the Assembler interface that is backed by +// a full wallet. This variant of the Assembler interface will produce the +// entirety of the funding transaction within the wallet. This implements the +// typical funding flow that is initiated either on the p2p level or using the +// CLi. +type WalletAssembler struct { + cfg WalletConfig +} + +// NewWalletAssembler creates a new instance of the WalletAssembler from a +// fully populated wallet config. +func NewWalletAssembler(cfg WalletConfig) *WalletAssembler { + return &WalletAssembler{ + cfg: cfg, + } +} + +// ProvisionChannel is the main entry point to begin a funding workflow given a +// fully populated request. The internal WalletAssembler will perform coin +// selection in a goroutine safe manner, returning an Intent that will allow +// the caller to finalize the funding process. +// +// NOTE: To cancel the funding flow the Cancel() method on the returned Intent, +// MUST be called. +// +// NOTE: This is a part of the chanfunding.Assembler interface. +func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { + var intent Intent + + // We hold the coin select mutex while querying for outputs, and + // performing coin selection in order to avoid inadvertent double + // spends across funding transactions. + err := w.cfg.CoinSelectLocker.WithCoinSelectLock(func() error { + log.Infof("Performing funding tx coin selection using %v "+ + "sat/kw as fee rate", int64(r.FeeRate)) + + // Find all unlocked unspent witness outputs that satisfy the + // minimum number of confirmations required. + coins, err := w.cfg.CoinSource.ListCoins( + r.MinConfs, math.MaxInt32, + ) + if err != nil { + return err + } + + var ( + selectedCoins []Coin + localContributionAmt btcutil.Amount + changeAmt btcutil.Amount + ) + + // Perform coin selection over our available, unlocked unspent + // outputs in order to find enough coins to meet the funding + // amount requirements. + switch { + // If there's no funding amount at all (receiving an inbound + // single funder request), then we don't need to perform any + // coin selection at all. + case r.LocalAmt == 0: + break + + // In case this request want the fees subtracted from the local + // amount, we'll call the specialized method for that. This + // ensures that we won't deduct more that the specified balance + // from our wallet. + case r.SubtractFees: + dustLimit := w.cfg.DustLimit + selectedCoins, localContributionAmt, changeAmt, err = CoinSelectSubtractFees( + r.FeeRate, r.LocalAmt, dustLimit, coins, + ) + if err != nil { + return err + } + + // Otherwise do a normal coin selection where we target a given + // funding amount. + default: + localContributionAmt = r.LocalAmt + selectedCoins, changeAmt, err = CoinSelect( + r.FeeRate, r.LocalAmt, coins, + ) + if err != nil { + return err + } + } + + // Record any change output(s) generated as a result of the + // coin selection, but only if the addition of the output won't + // lead to the creation of dust. + var changeOutput *wire.TxOut + if changeAmt != 0 && changeAmt > w.cfg.DustLimit { + changeAddr, err := r.ChangeAddr() + if err != nil { + return err + } + changeScript, err := txscript.PayToAddrScript(changeAddr) + if err != nil { + return err + } + + changeOutput = &wire.TxOut{ + Value: int64(changeAmt), + PkScript: changeScript, + } + } + + // Lock the selected coins. These coins are now "reserved", + // this prevents concurrent funding requests from referring to + // and this double-spending the same set of coins. + for _, coin := range selectedCoins { + outpoint := coin.OutPoint + + w.cfg.CoinLocker.LockOutpoint(outpoint) + } + + newIntent := &FullIntent{ + ShimIntent: ShimIntent{ + localFundingAmt: localContributionAmt, + remoteFundingAmt: r.RemoteAmt, + }, + InputCoins: selectedCoins, + coinLocker: w.cfg.CoinLocker, + coinSource: w.cfg.CoinSource, + signer: w.cfg.Signer, + } + + if changeOutput != nil { + newIntent.ChangeOutputs = []*wire.TxOut{changeOutput} + } + + intent = newIntent + + return nil + }) + if err != nil { + return nil, err + } + + return intent, nil +} + +// FundingTxAvailable is an empty method that an assembler can implement to +// signal to callers that its able to provide the funding transaction for the +// channel via the intent it returns. +// +// NOTE: This method is a part of the FundingTxAssembler interface. +func (w *WalletAssembler) FundingTxAvailable() {} + +// A compile-time assertion to ensure the WalletAssembler meets the +// FundingTxAssembler interface. +var _ FundingTxAssembler = (*WalletAssembler)(nil) diff --git a/log.go b/log.go index a6d81f262e..0ad511f4c6 100644 --- a/log.go +++ b/log.go @@ -26,6 +26,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/monitoring" "github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/peernotifier" @@ -96,6 +97,7 @@ func init() { addSubLogger("PROM", monitoring.UseLogger) addSubLogger("WTCL", wtclient.UseLogger) addSubLogger("PRNF", peernotifier.UseLogger) + addSubLogger("CHFD", chanfunding.UseLogger) addSubLogger(routerrpc.Subsystem, routerrpc.UseLogger) addSubLogger(wtclientrpc.Subsystem, wtclientrpc.UseLogger) From 7a64a7d3a42ba0ad63455769faedf067215184b1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:39:17 -0700 Subject: [PATCH 06/12] lnwallet: delegate all channel funding logic to the new chanfunding package In this commit, we begin to integrate the new channel funding package into the existing codebase. With this set of changes, we'll no longer construct and sign the funding transaction within this package, instead delegating it to the new chanfunding package. We use the new chanfunding.WalletAssembler to carry out all channel funding, providing it with an implementation of all its interfaces backed by the wallet. --- fundingmanager_test.go | 4 +- lnwallet/interface_test.go | 7 +- lnwallet/reservation.go | 19 +- lnwallet/wallet.go | 540 +++++++++++++++++-------------------- mock.go | 7 +- 5 files changed, 280 insertions(+), 297 deletions(-) diff --git a/fundingmanager_test.go b/fundingmanager_test.go index 09bbe6605f..80d3cd334e 100644 --- a/fundingmanager_test.go +++ b/fundingmanager_test.go @@ -2864,7 +2864,7 @@ func TestFundingManagerFundAll(t *testing.T) { Value: btcutil.Amount( 0.05 * btcutil.SatoshiPerBitcoin, ), - PkScript: make([]byte, 22), + PkScript: coinPkScript, OutPoint: wire.OutPoint{ Hash: chainhash.Hash{}, Index: 0, @@ -2875,7 +2875,7 @@ func TestFundingManagerFundAll(t *testing.T) { Value: btcutil.Amount( 0.06 * btcutil.SatoshiPerBitcoin, ), - PkScript: make([]byte, 22), + PkScript: coinPkScript, OutPoint: wire.OutPoint{ Hash: chainhash.Hash{}, Index: 1, diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 8f8e1748fa..3f31af6d91 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -41,6 +41,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" ) @@ -616,7 +617,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness, if err == nil { t.Fatalf("not error returned, should fail on coin selection") } - if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok { + if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok { t.Fatalf("error not coinselect error: %v", err) } if failedReservation != nil { @@ -655,7 +656,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness, // Attempt to create another channel with 44 BTC, this should fail. _, err = alice.InitChannelReservation(req) - if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok { + if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok { t.Fatalf("coin selection succeeded should have insufficient funds: %v", err) } @@ -699,7 +700,7 @@ func testCancelNonExistentReservation(miner *rpctest.Harness, // Create our own reservation, give it some ID. res, err := lnwallet.NewChannelReservation( 10000, 10000, feePerKw, alice, 22, 10, &testHdSeed, - lnwire.FFAnnounceChannel, true, + lnwire.FFAnnounceChannel, true, nil, [32]byte{}, ) if err != nil { t.Fatalf("unable to create res: %v", err) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index e1231e4526..4447e845ba 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" ) @@ -114,7 +115,10 @@ type ChannelReservation struct { // commitment state. pushMSat lnwire.MilliSatoshi - wallet *LightningWallet + wallet *LightningWallet + chanFunder chanfunding.Assembler + + fundingIntent chanfunding.Intent } // NewChannelReservation creates a new channel reservation. This function is @@ -124,8 +128,8 @@ type ChannelReservation struct { func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet, id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash, - flags lnwire.FundingFlag, - tweaklessCommit bool) (*ChannelReservation, error) { + flags lnwire.FundingFlag, tweaklessCommit bool, + fundingAssembler chanfunding.Assembler) (*ChannelReservation, error) { var ( ourBalance lnwire.MilliSatoshi @@ -213,6 +217,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, } else { chanType |= channeldb.SingleFunderBit } + + // If this intent isn't one that's able to provide us with a + // funding transaction, then we'll set the chanType bit to + // signal that we don't have access to one. + if _, ok := fundingAssembler.(chanfunding.FundingTxAssembler); !ok { + chanType |= channeldb.NoFundingTxBit + } + } else { // Otherwise, this is a dual funder channel, and no side is // technically the "initiator" @@ -253,6 +265,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, pushMSat: pushMSat, reservationID: id, wallet: wallet, + chanFunder: fundingAssembler, }, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index f5a44f5c39..1704e015fd 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "errors" "fmt" - "math" "net" "sync" "sync/atomic" @@ -99,6 +98,12 @@ type InitFundingReserveMsg struct { // commitment format or not. Tweakless bool + // ChanFunder is an optional channel funder that allows the caller to + // control exactly how the channel funding is carried out. If not + // specified, then the default chanfunding.WalletAssembler will be + // used. + ChanFunder chanfunding.Assembler + // err is a channel in which all errors will be sent across. Will be // nil if this initial set is successful. // @@ -448,11 +453,25 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg return } + // If no chanFunder was provided, then we'll assume the default + // assembler, which is backed by the wallet's internal coin selection. + if req.ChanFunder == nil { + cfg := chanfunding.WalletConfig{ + CoinSource: &CoinSource{l}, + CoinSelectLocker: l, + CoinLocker: l, + Signer: l.Cfg.Signer, + DustLimit: DefaultDustLimit(), + } + req.ChanFunder = chanfunding.NewWalletAssembler(cfg) + } + localFundingAmt := req.LocalFundingAmt + remoteFundingAmt := req.RemoteFundingAmt var ( - selected *coinSelection - err error + fundingIntent chanfunding.Intent + err error ) // If we're on the receiving end of a single funder channel then we @@ -463,9 +482,18 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // Coin selection is done on the basis of sat/kw, so we'll use // the fee rate passed in to perform coin selection. var err error - selected, err = l.selectCoinsAndChange( - req.FundingFeePerKw, req.LocalFundingAmt, req.MinConfs, - req.SubtractFees, + fundingReq := &chanfunding.Request{ + RemoteAmt: req.RemoteFundingAmt, + LocalAmt: req.LocalFundingAmt, + MinConfs: req.MinConfs, + SubtractFees: req.SubtractFees, + FeeRate: req.FundingFeePerKw, + ChangeAddr: func() (btcutil.Address, error) { + return l.NewAddress(WitnessPubKey, true) + }, + } + fundingIntent, err = req.ChanFunder.ProvisionChannel( + fundingReq, ) if err != nil { req.err <- err @@ -473,31 +501,38 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg return } - localFundingAmt = selected.fundingAmt + localFundingAmt = fundingIntent.LocalFundingAmt() + remoteFundingAmt = fundingIntent.RemoteFundingAmt() } // The total channel capacity will be the size of the funding output we // created plus the remote contribution. - capacity := localFundingAmt + req.RemoteFundingAmt + capacity := localFundingAmt + remoteFundingAmt id := atomic.AddUint64(&l.nextFundingID, 1) reservation, err := NewChannelReservation( capacity, localFundingAmt, req.CommitFeePerKw, l, id, req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, - req.Tweakless, + req.Tweakless, req.ChanFunder, ) if err != nil { - selected.unlockCoins() + if fundingIntent != nil { + fundingIntent.Cancel() + } + req.err <- err req.resp <- nil return } err = l.initOurContribution( - reservation, selected, req.NodeAddr, req.NodeID, + reservation, fundingIntent, req.NodeAddr, req.NodeID, ) if err != nil { - selected.unlockCoins() + if fundingIntent != nil { + fundingIntent.Cancel() + } + req.err <- err req.resp <- nil return @@ -520,26 +555,34 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // and change reserved for the channel, and derives the keys to use for this // channel. func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, - selected *coinSelection, nodeAddr net.Addr, nodeID *btcec.PublicKey) error { + fundingIntent chanfunding.Intent, nodeAddr net.Addr, + nodeID *btcec.PublicKey) error { // Grab the mutex on the ChannelReservation to ensure thread-safety reservation.Lock() defer reservation.Unlock() - if selected != nil { - reservation.ourContribution.Inputs = selected.coins - reservation.ourContribution.ChangeOutputs = selected.change + // At this point, if we have a funding intent, we'll use it to populate + // the existing reservation state entries for our coin selection. + if fundingIntent != nil { + if intent, ok := fundingIntent.(*chanfunding.FullIntent); ok { + for _, coin := range intent.InputCoins { + reservation.ourContribution.Inputs = append( + reservation.ourContribution.Inputs, + &wire.TxIn{ + PreviousOutPoint: coin.OutPoint, + }, + ) + } + reservation.ourContribution.ChangeOutputs = intent.ChangeOutputs + } + + reservation.fundingIntent = fundingIntent } reservation.nodeAddr = nodeAddr reservation.partialState.IdentityPub = nodeID - // Next, we'll grab a series of keys from the wallet which will be used - // for the duration of the channel. The keys include: our multi-sig - // key, the base revocation key, the base htlc key,the base payment - // key, and the delayed payment key. - // - // TODO(roasbeef): "salt" each key as well? var err error reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey( keychain.KeyFamilyMultiSig, @@ -709,100 +752,73 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { pendingReservation.Lock() defer pendingReservation.Unlock() - // Create a blank, fresh transaction. Soon to be a complete funding - // transaction which will allow opening a lightning channel. - pendingReservation.fundingTx = wire.NewMsgTx(1) - fundingTx := pendingReservation.fundingTx - // Some temporary variables to cut down on the resolution verbosity. pendingReservation.theirContribution = req.contribution theirContribution := req.contribution ourContribution := pendingReservation.ourContribution - // Add all multi-party inputs and outputs to the transaction. - for _, ourInput := range ourContribution.Inputs { - fundingTx.AddTxIn(ourInput) - } - for _, theirInput := range theirContribution.Inputs { - fundingTx.AddTxIn(theirInput) - } - for _, ourChangeOutput := range ourContribution.ChangeOutputs { - fundingTx.AddTxOut(ourChangeOutput) - } - for _, theirChangeOutput := range theirContribution.ChangeOutputs { - fundingTx.AddTxOut(theirChangeOutput) - } - - ourKey := pendingReservation.ourContribution.MultiSigKey - theirKey := theirContribution.MultiSigKey - - // Finally, add the 2-of-2 multi-sig output which will set up the lightning - // channel. - channelCapacity := int64(pendingReservation.partialState.Capacity) - witnessScript, multiSigOut, err := input.GenFundingPkScript( - ourKey.PubKey.SerializeCompressed(), - theirKey.PubKey.SerializeCompressed(), channelCapacity, + var ( + chanPoint *wire.OutPoint + err error ) - if err != nil { - req.err <- err - return - } - // Sort the transaction. Since both side agree to a canonical ordering, - // by sorting we no longer need to send the entire transaction. Only - // signatures will be exchanged. - fundingTx.AddTxOut(multiSigOut) - txsort.InPlaceSort(pendingReservation.fundingTx) + // At this point, we can now construct our channel point. Depending on + // which type of intent we obtained from our chanfunding.Assembler, + // we'll carry out a distinct set of steps. + switch fundingIntent := pendingReservation.fundingIntent.(type) { + case *chanfunding.FullIntent: + // Now that we know their public key, we can bind theirs as + // well as ours to the funding intent. + fundingIntent.BindKeys( + &pendingReservation.ourContribution.MultiSigKey, + theirContribution.MultiSigKey.PubKey, + ) - // Next, sign all inputs that are ours, collecting the signatures in - // order of the inputs. - pendingReservation.ourFundingInputScripts = make([]*input.Script, 0, - len(ourContribution.Inputs)) - signDesc := input.SignDescriptor{ - HashType: txscript.SigHashAll, - SigHashes: txscript.NewTxSigHashes(fundingTx), - } - for i, txIn := range fundingTx.TxIn { - info, err := l.FetchInputInfo(&txIn.PreviousOutPoint) - if err == ErrNotMine { - continue - } else if err != nil { - req.err <- err + // With our keys bound, we can now construct+sign the final + // funding transaction and also obtain the chanPoint that + // creates the channel. + fundingTx, err := fundingIntent.CompileFundingTx( + theirContribution.Inputs, + theirContribution.ChangeOutputs, + ) + if err != nil { + req.err <- fmt.Errorf("unable to construct funding "+ + "tx: %v", err) return } - - signDesc.Output = &wire.TxOut{ - PkScript: info.PkScript, - Value: int64(info.Value), - } - signDesc.InputIndex = i - - inputScript, err := l.Cfg.Signer.ComputeInputScript( - fundingTx, &signDesc, - ) + chanPoint, err = fundingIntent.ChanPoint() if err != nil { - req.err <- err + req.err <- fmt.Errorf("unable to obtain chan "+ + "point: %v", err) return } - txIn.SignatureScript = inputScript.SigScript - txIn.Witness = inputScript.Witness - pendingReservation.ourFundingInputScripts = append( - pendingReservation.ourFundingInputScripts, - inputScript, + // Finally, we'll populate the relevant information in our + // pendingReservation so the rest of the funding flow can + // continue as normal. + pendingReservation.fundingTx = fundingTx + pendingReservation.partialState.FundingOutpoint = *chanPoint + pendingReservation.ourFundingInputScripts = make( + []*input.Script, 0, len(ourContribution.Inputs), ) - } + for _, txIn := range fundingTx.TxIn { + _, err := l.FetchInputInfo(&txIn.PreviousOutPoint) + if err != nil { + continue + } - // Locate the index of the multi-sig outpoint in order to record it - // since the outputs are canonically sorted. If this is a single funder - // workflow, then we'll also need to send this to the remote node. - fundingTxID := fundingTx.TxHash() - _, multiSigIndex := input.FindScriptOutputIndex(fundingTx, multiSigOut.PkScript) - fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex) - pendingReservation.partialState.FundingOutpoint = *fundingOutpoint + pendingReservation.ourFundingInputScripts = append( + pendingReservation.ourFundingInputScripts, + &input.Script{ + Witness: txIn.Witness, + SigScript: txIn.SignatureScript, + }, + ) + } - walletLog.Debugf("Funding tx for ChannelPoint(%v) generated: %v", - fundingOutpoint, spew.Sdump(fundingTx)) + walletLog.Debugf("Funding tx for ChannelPoint(%v) "+ + "generated: %v", chanPoint, spew.Sdump(fundingTx)) + } // Initialize an empty sha-chain for them, tracking the current pending // revocation hash (we don't yet know the preimage so we can't add it @@ -819,10 +835,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // Create the txin to our commitment transaction; required to construct // the commitment transactions. fundingTxIn := wire.TxIn{ - PreviousOutPoint: wire.OutPoint{ - Hash: fundingTxID, - Index: multiSigIndex, - }, + PreviousOutPoint: *chanPoint, } // With the funding tx complete, create both commitment transactions. @@ -879,21 +892,32 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { txsort.InPlaceSort(theirCommitTx) walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v", - fundingOutpoint, spew.Sdump(ourCommitTx)) + chanPoint, spew.Sdump(ourCommitTx)) walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v", - fundingOutpoint, spew.Sdump(theirCommitTx)) + chanPoint, spew.Sdump(theirCommitTx)) // Record newly available information within the open channel state. - chanState.FundingOutpoint = *fundingOutpoint + chanState.FundingOutpoint = *chanPoint chanState.LocalCommitment.CommitTx = ourCommitTx chanState.RemoteCommitment.CommitTx = theirCommitTx + // Next, we'll obtain the funding witness script, and the funding + // output itself so we can generate a valid signature for the remote + // party. + fundingIntent := pendingReservation.fundingIntent + fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput() + if err != nil { + req.err <- fmt.Errorf("unable to obtain funding output") + return + } + // Generate a signature for their version of the initial commitment // transaction. - signDesc = input.SignDescriptor{ - WitnessScript: witnessScript, + ourKey := ourContribution.MultiSigKey + signDesc := input.SignDescriptor{ + WitnessScript: fundingWitnessScript, KeyDesc: ourKey, - Output: multiSigOut, + Output: fundingOutput, HashType: txscript.SigHashAll, SigHashes: txscript.NewTxSigHashes(theirCommitTx), InputIndex: 0, @@ -949,34 +973,14 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg return } +// verifyFundingInputs attempts to verify all remote inputs to the funding +// transaction. +func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx, + remoteInputScripts []*input.Script) error { -// handleFundingCounterPartySigs is the final step in the channel reservation -// workflow. During this step, we validate *all* the received signatures for -// inputs to the funding transaction. If any of these are invalid, we bail, -// and forcibly cancel this funding request. Additionally, we ensure that the -// signature we received from the counterparty for our version of the commitment -// transaction allows us to spend from the funding output with the addition of -// our signature. -func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigsMsg) { - l.limboMtx.RLock() - res, ok := l.fundingLimbo[msg.pendingFundingID] - l.limboMtx.RUnlock() - if !ok { - msg.err <- fmt.Errorf("attempted to update non-existent funding state") - return - } - - // Grab the mutex on the ChannelReservation to ensure thread-safety - res.Lock() - defer res.Unlock() - - // Now we can complete the funding transaction by adding their - // signatures to their inputs. - res.theirFundingInputScripts = msg.theirFundingInputScripts - inputScripts := msg.theirFundingInputScripts - fundingTx := res.fundingTx sigIndex := 0 fundingHashCache := txscript.NewTxSigHashes(fundingTx) + inputScripts := remoteInputScripts for i, txin := range fundingTx.TxIn { if len(inputScripts) != 0 && len(txin.Witness) == 0 { // Attach the input scripts so we can verify it below. @@ -992,10 +996,7 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs txin.Witness[len(txin.Witness)-1], ) if err != nil { - msg.err <- fmt.Errorf("cannot create script: "+ - "%v", err) - msg.completeChan <- nil - return + return fmt.Errorf("cannot create script: %v", err) } output, err := l.Cfg.ChainIO.GetUtxo( @@ -1003,33 +1004,70 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs pkScript, 0, l.quit, ) if output == nil { - msg.err <- fmt.Errorf("input to funding tx "+ - "does not exist: %v", err) - msg.completeChan <- nil - return + return fmt.Errorf("input to funding tx does "+ + "not exist: %v", err) } // Ensure that the witness+sigScript combo is valid. - vm, err := txscript.NewEngine(output.PkScript, - fundingTx, i, txscript.StandardVerifyFlags, nil, - fundingHashCache, output.Value) + vm, err := txscript.NewEngine( + output.PkScript, fundingTx, i, + txscript.StandardVerifyFlags, nil, + fundingHashCache, output.Value, + ) if err != nil { - msg.err <- fmt.Errorf("cannot create script "+ + return fmt.Errorf("cannot create script "+ "engine: %s", err) - msg.completeChan <- nil - return } if err = vm.Execute(); err != nil { - msg.err <- fmt.Errorf("cannot validate "+ + return fmt.Errorf("cannot validate "+ "transaction: %s", err) - msg.completeChan <- nil - return } sigIndex++ } } + return nil +} + +// handleFundingCounterPartySigs is the final step in the channel reservation +// workflow. During this step, we validate *all* the received signatures for +// inputs to the funding transaction. If any of these are invalid, we bail, +// and forcibly cancel this funding request. Additionally, we ensure that the +// signature we received from the counterparty for our version of the commitment +// transaction allows us to spend from the funding output with the addition of +// our signature. +func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigsMsg) { + l.limboMtx.RLock() + res, ok := l.fundingLimbo[msg.pendingFundingID] + l.limboMtx.RUnlock() + if !ok { + msg.err <- fmt.Errorf("attempted to update non-existent funding state") + return + } + + // Grab the mutex on the ChannelReservation to ensure thread-safety + res.Lock() + defer res.Unlock() + + // Now we can complete the funding transaction by adding their + // signatures to their inputs. + res.theirFundingInputScripts = msg.theirFundingInputScripts + inputScripts := msg.theirFundingInputScripts + + // Only if we have the final funding transaction do we need to verify + // the final set of inputs. Otherwise, it may be the case that the + // channel was funded via an external wallet. + fundingTx := res.fundingTx + if res.partialState.ChanType.HasFundingTx() { + err := l.verifyFundingInputs(fundingTx, inputScripts) + if err != nil { + msg.err <- err + msg.completeChan <- nil + return + } + } + // At this point, we can also record and verify their signature for our // commitment transaction. res.theirCommitmentSig = msg.theirCommitmentSig @@ -1055,8 +1093,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // is complete, allowing us to spend from the funding transaction. channelValue := int64(res.partialState.Capacity) hashCache := txscript.NewTxSigHashes(commitTx) - sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, - txscript.SigHashAll, commitTx, 0, channelValue) + sigHash, err := txscript.CalcWitnessSigHash( + witnessScript, hashCache, txscript.SigHashAll, commitTx, + 0, channelValue, + ) if err != nil { msg.err <- err msg.completeChan <- nil @@ -1203,8 +1243,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { return } - sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, - txscript.SigHashAll, ourCommitTx, 0, channelValue) + sigHash, err := txscript.CalcWitnessSigHash( + witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0, + channelValue, + ) if err != nil { req.err <- err req.completeChan <- nil @@ -1218,7 +1260,8 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { req.err <- err req.completeChan <- nil return - } else if !sig.Verify(sigHash, theirKey.PubKey) { + } + if !sig.Verify(sigHash, theirKey.PubKey) { req.err <- fmt.Errorf("counterparty's commitment signature " + "is invalid") req.completeChan <- nil @@ -1291,138 +1334,6 @@ func (l *LightningWallet) WithCoinSelectLock(f func() error) error { return f() } -// coinSelection holds the result from selectCoinsAndChange. -type coinSelection struct { - coins []*wire.TxIn - change []*wire.TxOut - fundingAmt btcutil.Amount - unlockCoins func() -} - -// selectCoinsAndChange performs coin selection in order to obtain witness -// outputs which sum to at least 'amt' amount of satoshis. If necessary, -// a change address will also be generated. If coin selection is -// successful/possible, then the selected coins and change outputs are -// returned, and the value of the resulting funding output. This method locks -// the selected outputs, and a function closure to unlock them in case of an -// error is returned. -func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight, - amt btcutil.Amount, minConfs int32, subtractFees bool) ( - *coinSelection, error) { - - // We hold the coin select mutex while querying for outputs, and - // performing coin selection in order to avoid inadvertent double - // spends across funding transactions. - l.coinSelectMtx.Lock() - defer l.coinSelectMtx.Unlock() - - walletLog.Infof("Performing funding tx coin selection using %v "+ - "sat/kw as fee rate", int64(feeRate)) - - // Find all unlocked unspent witness outputs that satisfy the minimum - // number of confirmations required. - utxos, err := l.ListUnspentWitness(minConfs, math.MaxInt32) - if err != nil { - return nil, err - } - - coins := make([]chanfunding.Coin, len(utxos), 0) - for _, utxo := range utxos { - coins = append(coins, chanfunding.Coin{ - TxOut: wire.TxOut{ - Value: int64(utxo.Value), - PkScript: utxo.PkScript, - }, - OutPoint: utxo.OutPoint, - }) - } - - var ( - selectedCoins []chanfunding.Coin - fundingAmt btcutil.Amount - changeAmt btcutil.Amount - ) - - // Perform coin selection over our available, unlocked unspent outputs - // in order to find enough coins to meet the funding amount - // requirements. - switch { - // In case this request want the fees subtracted from the local amount, - // we'll call the specialized method for that. This ensures that we - // won't deduct more that the specified balance from our wallet. - case subtractFees: - dustLimit := l.Cfg.DefaultConstraints.DustLimit - selectedCoins, fundingAmt, changeAmt, err = chanfunding.CoinSelectSubtractFees( - feeRate, amt, dustLimit, coins, - ) - if err != nil { - return nil, err - } - - // Ótherwise do a normal coin selection where we target a given funding - // amount. - default: - fundingAmt = amt - selectedCoins, changeAmt, err = chanfunding.CoinSelect(feeRate, amt, coins) - if err != nil { - return nil, err - } - } - - // Record any change output(s) generated as a result of the coin - // selection, but only if the addition of the output won't lead to the - // creation of dust. - var changeOutputs []*wire.TxOut - if changeAmt != 0 && changeAmt > DefaultDustLimit() { - changeAddr, err := l.NewAddress(WitnessPubKey, true) - if err != nil { - return nil, err - } - changeScript, err := txscript.PayToAddrScript(changeAddr) - if err != nil { - return nil, err - } - - changeOutputs = make([]*wire.TxOut, 1) - changeOutputs[0] = &wire.TxOut{ - Value: int64(changeAmt), - PkScript: changeScript, - } - } - - // Lock the selected coins. These coins are now "reserved", this - // prevents concurrent funding requests from referring to and this - // double-spending the same set of coins. - inputs := make([]*wire.TxIn, len(selectedCoins)) - for i, coin := range selectedCoins { - outpoint := &coin.OutPoint - l.lockedOutPoints[*outpoint] = struct{}{} - l.LockOutpoint(*outpoint) - - // Empty sig script, we'll actually sign if this reservation is - // queued up to be completed (the other side accepts). - inputs[i] = wire.NewTxIn(outpoint, nil, nil) - } - - unlock := func() { - l.coinSelectMtx.Lock() - defer l.coinSelectMtx.Unlock() - - for _, coin := range selectedCoins { - outpoint := &coin.OutPoint - delete(l.lockedOutPoints, *outpoint) - l.UnlockOutpoint(*outpoint) - } - } - - return &coinSelection{ - coins: inputs, - change: changeOutputs, - fundingAmt: fundingAmt, - unlockCoins: unlock, - }, nil -} - // DeriveStateHintObfuscator derives the bytes to be used for obfuscating the // state hints from the root to be used for a new channel. The obfuscator is // generated via the following computation: @@ -1513,3 +1424,56 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, return nil } + +// CoinSource is a wrapper around the wallet that implements the +// chanfunding.CoinSource interface. +type CoinSource struct { + wallet *LightningWallet +} + +// NewCoinSource creates a new instance of the CoinSource wrapper struct. +func NewCoinSource(w *LightningWallet) *CoinSource { + return &CoinSource{wallet: w} +} + +// 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) { + + utxos, err := c.wallet.ListUnspentWitness(minConfs, maxConfs) + if err != nil { + return nil, err + } + + var coins []chanfunding.Coin + for _, utxo := range utxos { + coins = append(coins, chanfunding.Coin{ + TxOut: wire.TxOut{ + Value: int64(utxo.Value), + PkScript: utxo.PkScript, + }, + OutPoint: utxo.OutPoint, + }) + } + + return coins, nil +} + +// 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) { + inputInfo, err := c.wallet.FetchInputInfo(&op) + if err != nil { + return nil, err + } + + return &chanfunding.Coin{ + TxOut: wire.TxOut{ + Value: int64(inputInfo.Value), + PkScript: inputInfo.PkScript, + }, + OutPoint: inputInfo.OutPoint, + }, nil +} diff --git a/mock.go b/mock.go index 609b781860..bf9559d747 100644 --- a/mock.go +++ b/mock.go @@ -1,6 +1,7 @@ package lnd import ( + "encoding/hex" "fmt" "sync" "sync/atomic" @@ -20,6 +21,10 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +var ( + coinPkScript, _ = hex.DecodeString("001431df1bde03c074d0cf21ea2529427e1499b8f1de") +) + // The block height returned by the mock BlockChainIO's GetBestBlock. const fundingBroadcastHeight = 123 @@ -297,7 +302,7 @@ func (m *mockWalletController) ListUnspentWitness(minconfirms, utxo := &lnwallet.Utxo{ AddressType: lnwallet.WitnessPubKey, Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin), - PkScript: make([]byte, 22), + PkScript: coinPkScript, OutPoint: wire.OutPoint{ Hash: chainhash.Hash{}, Index: m.index, From c3157ae2c4d64da82cf0ebb46bf3f8a4fce9ec51 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:44:16 -0700 Subject: [PATCH 07/12] lnwallet: add awareness of chanfunding.ShimIntents In this commit, we make the wallet aware of the second type of funding intent: the ShimIntent. If we have one of these, then we don't need to construct the funding transaction, and can instead just obtain the outpoint directly from it. --- lnwallet/wallet.go | 63 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 1704e015fd..c59de2c51b 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -525,8 +525,21 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg return } + var keyRing keychain.KeyRing = l.SecretKeyRing + + // If this is a shim intent, then it may be attempting to use an + // existing set of keys for the funding workflow. In this case, we'll + // make a simple wrapper keychain.KeyRing that will proxy certain + // derivation calls to future callers. + if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok { + keyRing = &shimKeyRing{ + KeyRing: keyRing, + ShimIntent: shimIntent, + } + } + err = l.initOurContribution( - reservation, fundingIntent, req.NodeAddr, req.NodeID, + reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing, ) if err != nil { if fundingIntent != nil { @@ -556,7 +569,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // channel. func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, fundingIntent chanfunding.Intent, nodeAddr net.Addr, - nodeID *btcec.PublicKey) error { + nodeID *btcec.PublicKey, keyRing keychain.KeyRing) error { // Grab the mutex on the ChannelReservation to ensure thread-safety reservation.Lock() @@ -584,31 +597,31 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, reservation.partialState.IdentityPub = nodeID var err error - reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey( + reservation.ourContribution.MultiSigKey, err = keyRing.DeriveNextKey( keychain.KeyFamilyMultiSig, ) if err != nil { return err } - reservation.ourContribution.RevocationBasePoint, err = l.DeriveNextKey( + reservation.ourContribution.RevocationBasePoint, err = keyRing.DeriveNextKey( keychain.KeyFamilyRevocationBase, ) if err != nil { return err } - reservation.ourContribution.HtlcBasePoint, err = l.DeriveNextKey( + reservation.ourContribution.HtlcBasePoint, err = keyRing.DeriveNextKey( keychain.KeyFamilyHtlcBase, ) if err != nil { return err } - reservation.ourContribution.PaymentBasePoint, err = l.DeriveNextKey( + reservation.ourContribution.PaymentBasePoint, err = keyRing.DeriveNextKey( keychain.KeyFamilyPaymentBase, ) if err != nil { return err } - reservation.ourContribution.DelayBasePoint, err = l.DeriveNextKey( + reservation.ourContribution.DelayBasePoint, err = keyRing.DeriveNextKey( keychain.KeyFamilyDelayBase, ) if err != nil { @@ -617,7 +630,7 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, // With the above keys created, we'll also need to initialization our // initial revocation tree state. - nextRevocationKeyDesc, err := l.DeriveNextKey( + nextRevocationKeyDesc, err := keyRing.DeriveNextKey( keychain.KeyFamilyRevocationRoot, ) if err != nil { @@ -766,6 +779,15 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // which type of intent we obtained from our chanfunding.Assembler, // we'll carry out a distinct set of steps. switch fundingIntent := pendingReservation.fundingIntent.(type) { + case *chanfunding.ShimIntent: + chanPoint, err = fundingIntent.ChanPoint() + if err != nil { + req.err <- fmt.Errorf("unable to obtain chan point: %v", err) + return + } + + pendingReservation.partialState.FundingOutpoint = *chanPoint + case *chanfunding.FullIntent: // Now that we know their public key, we can bind theirs as // well as ours to the funding intent. @@ -1477,3 +1499,28 @@ func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, erro OutPoint: inputInfo.OutPoint, }, nil } + +// shimKeyRing is a wrapper struct that's used to provide the proper multi-sig +// key for an initiated external funding flow. +type shimKeyRing struct { + keychain.KeyRing + + *chanfunding.ShimIntent +} + +// DeriveNextKey intercepts the normal DeriveNextKey call to a keychain.KeyRing +// instance, and supplies the multi-sig key specified by the ShimIntent. This +// allows us to transparently insert new keys into the existing funding flow, +// as these keys may not come from the wallet itself. +func (s *shimKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) { + if keyFam != keychain.KeyFamilyMultiSig { + return s.KeyRing.DeriveNextKey(keyFam) + } + + fundingKeys, err := s.ShimIntent.MultiSigKeys() + if err != nil { + return keychain.KeyDescriptor{}, err + } + + return *fundingKeys.LocalKey, nil +} From 6e9cbc19f95ecc4e4f40e91fe72af0bb18bb887b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:45:44 -0700 Subject: [PATCH 08/12] lnwallet+funding: pass the pending channel ID into the reservation context In this commit, we start to thread the pending channel ID from wire protocol all the way down into the reservation context. This change will allow negotiation to take place _outside_ the protocol that may result in a particular chanfunding.Assembler being dispatched. --- fundingmanager.go | 12 +++++++----- lnwallet/reservation.go | 8 +++++++- lnwallet/wallet.go | 6 +++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/fundingmanager.go b/fundingmanager.go index 760687fdd5..e87dd9f118 100644 --- a/fundingmanager.go +++ b/fundingmanager.go @@ -1216,6 +1216,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) { chainHash := chainhash.Hash(msg.ChainHash) req := &lnwallet.InitFundingReserveMsg{ ChainHash: &chainHash, + PendingChanID: msg.PendingChannelID, NodeID: fmsg.peer.IdentityKey(), NodeAddr: fmsg.peer.Address(), LocalFundingAmt: 0, @@ -2781,6 +2782,10 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { channelFlags = lnwire.FFAnnounceChannel } + // Obtain a new pending channel ID which is used to track this + // reservation throughout its lifetime. + chanID := f.nextPendingChanID() + // Initialize a funding reservation with the local wallet. If the // wallet doesn't have enough funds to commit to this channel, then the // request will fail, and be aborted. @@ -2798,6 +2803,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { tweaklessCommitment := localTweakless && remoteTweakless req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.chainHash, + PendingChanID: chanID, NodeID: peerKey, NodeAddr: msg.peer.Address(), SubtractFees: msg.subtractFees, @@ -2823,11 +2829,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) { // SubtractFees=true. capacity := reservation.Capacity() - // Obtain a new pending channel ID which is used to track this - // reservation throughout its lifetime. - chanID := f.nextPendingChanID() - - fndgLog.Infof("Target commit tx sat/kw for pending_id(%x): %v", chanID, + fndgLog.Infof("Target commit tx sat/kw for pendingID(%x): %v", chanID, int64(commitFeePerKw)) // If the remote CSV delay was not set in the open channel request, diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 4447e845ba..19f49eeae3 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -110,6 +110,10 @@ type ChannelReservation struct { // throughout its lifetime. reservationID uint64 + // pendingChanID is the pending channel ID for this channel as + // identified within the wire protocol. + pendingChanID [32]byte + // pushMSat the amount of milli-satoshis that should be pushed to the // responder of a single funding channel as part of the initial // commitment state. @@ -129,7 +133,8 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet, id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash, flags lnwire.FundingFlag, tweaklessCommit bool, - fundingAssembler chanfunding.Assembler) (*ChannelReservation, error) { + fundingAssembler chanfunding.Assembler, + pendingChanID [32]byte) (*ChannelReservation, error) { var ( ourBalance lnwire.MilliSatoshi @@ -263,6 +268,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, Db: wallet.Cfg.Database, }, pushMSat: pushMSat, + pendingChanID: pendingChanID, reservationID: id, wallet: wallet, chanFunder: fundingAssembler, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index c59de2c51b..0851f207d2 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -48,6 +48,10 @@ type InitFundingReserveMsg struct { // target channel. ChainHash *chainhash.Hash + // PendingChanID is the pending channel ID for this funding flow as + // used in the wire protocol. + PendingChanID [32]byte + // NodeID is the ID of the remote node we would like to open a channel // with. NodeID *btcec.PublicKey @@ -513,7 +517,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg reservation, err := NewChannelReservation( capacity, localFundingAmt, req.CommitFeePerKw, l, id, req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, - req.Tweakless, req.ChanFunder, + req.Tweakless, req.ChanFunder, req.PendingChanID, ) if err != nil { if fundingIntent != nil { From 9926259da0e72d2e6b8e7609907c1e97b6414c50 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:47:27 -0700 Subject: [PATCH 09/12] lnwallet: add new RegisterFundingIntent method In this commit, we add a new method `RegisterFundingIntent` that allows a caller to "inject" a pre-populated chanfunding.Intent into a funding workflow. As an example, if we've already agreed upon the "shape" of the funding output _outside_ the protocol, then we can use this to pass down the details of the output, then leverage the normal wire protocol to carry out the remainder of the funding flow. --- lnwallet/wallet.go | 57 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 0851f207d2..55ef712359 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -277,6 +277,11 @@ type LightningWallet struct { // the currently locked outpoints. lockedOutPoints map[wire.OutPoint]struct{} + // fundingIntents houses all the "interception" registered by a caller + // using the RegisterFundingIntent method. + intentMtx sync.RWMutex + fundingIntents map[[32]byte]chanfunding.Intent + quit chan struct{} wg sync.WaitGroup @@ -297,6 +302,7 @@ func NewLightningWallet(Cfg Config) (*LightningWallet, error) { nextFundingID: 0, fundingLimbo: make(map[uint64]*ChannelReservation), lockedOutPoints: make(map[wire.OutPoint]struct{}), + fundingIntents: make(map[[32]byte]chanfunding.Intent), quit: make(chan struct{}), }, nil } @@ -435,6 +441,21 @@ func (l *LightningWallet) InitChannelReservation( return <-req.resp, <-req.err } +// RegisterFundingIntent allows a caller to signal to the wallet that if a +// pending channel ID of expectedID is found, then it can skip constructing a +// new chanfunding.Assembler, and instead use the specified chanfunding.Intent. +// As an example, this lets some of the parameters for funding transaction to +// be negotiated outside the regular funding protocol. +func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte, + shimIntent chanfunding.Intent) error { + + l.intentMtx.Lock() + l.fundingIntents[expectedID] = shimIntent + l.intentMtx.Unlock() + + return nil +} + // handleFundingReserveRequest processes a message intending to create, and // validate a funding reservation request. func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) { @@ -478,11 +499,19 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg err error ) - // If we're on the receiving end of a single funder channel then we - // don't need to perform any coin selection, and the remote contributes - // all funds. Otherwise, attempt to obtain enough coins to meet the - // required funding amount. - if req.LocalFundingAmt != 0 { + // If we've just received an inbound funding request that we have a + // registered shim intent to, then we'll obtain the backing intent now. + // In this case, we're doing a special funding workflow that allows + // more advanced constructions such as channel factories to be + // instantiated. + l.intentMtx.Lock() + fundingIntent, ok := l.fundingIntents[req.PendingChanID] + l.intentMtx.Unlock() + + // Otherwise, this is a normal funding flow, so we'll use the chan + // funder in the attached request to provision the inputs/outputs + // that'll ultimately be used to construct the funding transaction. + if !ok { // Coin selection is done on the basis of sat/kw, so we'll use // the fee rate passed in to perform coin selection. var err error @@ -700,6 +729,16 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs delete(l.fundingLimbo, req.pendingFundingID) + pid := pendingReservation.pendingChanID + + l.intentMtx.Lock() + if intent, ok := l.fundingIntents[pid]; ok { + intent.Cancel() + + delete(l.fundingIntents, pendingReservation.pendingChanID) + } + l.intentMtx.Unlock() + req.err <- nil } @@ -1149,6 +1188,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs delete(l.fundingLimbo, res.reservationID) l.limboMtx.Unlock() + l.intentMtx.Lock() + delete(l.fundingIntents, res.pendingChanID) + l.intentMtx.Unlock() + // As we're about to broadcast the funding transaction, we'll take note // of the current height for record keeping purposes. // @@ -1347,6 +1390,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { l.limboMtx.Lock() delete(l.fundingLimbo, req.pendingFundingID) l.limboMtx.Unlock() + + l.intentMtx.Lock() + delete(l.fundingIntents, pendingReservation.pendingChanID) + l.intentMtx.Unlock() } // WithCoinSelectLock will execute the passed function closure in a From c3a7da5ce7d5f703ea206a28931ed4920f85c74d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:47:50 -0700 Subject: [PATCH 10/12] lnwallet: add new test to exercise external channel funding --- lnwallet/interface_test.go | 163 +++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 15 deletions(-) diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 3f31af6d91..2622f56bca 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -793,7 +793,9 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi } func testSingleFunderReservationWorkflow(miner *rpctest.Harness, - alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool) { + alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool, + aliceChanFunder chanfunding.Assembler, + fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) { // For this scenario, Alice will be the channel initiator while bob // will act as the responder to the workflow. @@ -812,6 +814,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, } aliceReq := &lnwallet.InitFundingReserveMsg{ ChainHash: chainHash, + PendingChanID: pendingChanID, NodeID: bobPub, NodeAddr: bobAddr, LocalFundingAmt: fundingAmt, @@ -821,6 +824,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, PushMSat: pushAmt, Flags: lnwire.FFAnnounceChannel, Tweakless: tweakless, + ChanFunder: aliceChanFunder, } aliceChanReservation, err := alice.InitChannelReservation(aliceReq) if err != nil { @@ -840,15 +844,20 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, t.Fatalf("unable to verify constraints: %v", err) } - // Verify all contribution fields have been set properly. + // Verify all contribution fields have been set properly, but only if + // Alice is the funder herself. aliceContribution := aliceChanReservation.OurContribution() - if len(aliceContribution.Inputs) < 1 { - t.Fatalf("outputs for funding tx not properly selected, have %v "+ - "outputs should at least 1", len(aliceContribution.Inputs)) - } - if len(aliceContribution.ChangeOutputs) != 1 { - t.Fatalf("coin selection failed, should have one change outputs, "+ - "instead have: %v", len(aliceContribution.ChangeOutputs)) + if fetchFundingTx == nil { + if len(aliceContribution.Inputs) < 1 { + t.Fatalf("outputs for funding tx not properly "+ + "selected, have %v outputs should at least 1", + len(aliceContribution.Inputs)) + } + if len(aliceContribution.ChangeOutputs) != 1 { + t.Fatalf("coin selection failed, should have one "+ + "change outputs, instead have: %v", + len(aliceContribution.ChangeOutputs)) + } } assertContributionInitPopulated(t, aliceContribution) @@ -856,6 +865,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // reservation initiation, then consume Alice's contribution. bobReq := &lnwallet.InitFundingReserveMsg{ ChainHash: chainHash, + PendingChanID: pendingChanID, NodeID: alicePub, NodeAddr: aliceAddr, LocalFundingAmt: 0, @@ -898,10 +908,11 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // At this point, Alice should have generated all the signatures // required for the funding transaction, as well as Alice's commitment - // signature to bob. + // signature to bob, but only if the funding transaction was + // constructed internally. aliceRemoteContribution := aliceChanReservation.TheirContribution() aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures() - if aliceFundingSigs == nil { + if fetchFundingTx == nil && aliceFundingSigs == nil { t.Fatalf("funding sigs not found") } if aliceCommitSig == nil { @@ -910,7 +921,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // Additionally, the funding tx and the funding outpoint should have // been populated. - if aliceChanReservation.FinalFundingTx() == nil { + if aliceChanReservation.FinalFundingTx() == nil && fetchFundingTx == nil { t.Fatalf("funding transaction never created!") } if aliceChanReservation.FundingOutpoint() == nil { @@ -952,9 +963,17 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, t.Fatalf("alice unable to complete reservation: %v", err) } + // If the caller provided an alternative way to obtain the funding tx, + // then we'll use that. Otherwise, we'll obtain it directly from Alice. + var fundingTx *wire.MsgTx + if fetchFundingTx != nil { + fundingTx = fetchFundingTx() + } else { + fundingTx = aliceChanReservation.FinalFundingTx() + } + // The resulting active channel state should have been persisted to the // DB for both Alice and Bob. - fundingTx := aliceChanReservation.FinalFundingTx() fundingSha := fundingTx.TxHash() aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub) if err != nil { @@ -2524,7 +2543,8 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, false, + miner, alice, bob, t, false, nil, nil, + [32]byte{}, ) }, }, @@ -2534,10 +2554,15 @@ var walletTests = []walletTestCase{ bob *lnwallet.LightningWallet, t *testing.T) { testSingleFunderReservationWorkflow( - miner, alice, bob, t, true, + miner, alice, bob, t, true, nil, nil, + [32]byte{}, ) }, }, + { + name: "single funding workflow external funding tx", + test: testSingleFunderExternalFundingTx, + }, { name: "dual funder workflow", test: testDualFundingReservationWorkflow, @@ -2676,6 +2701,114 @@ func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error { return nil } +// testSingleFunderExternalFundingTx tests that the wallet is able to properly +// carry out a funding flow backed by a channel point that has been crafted +// outside the wallet. +func testSingleFunderExternalFundingTx(miner *rpctest.Harness, + alice, bob *lnwallet.LightningWallet, t *testing.T) { + + // First, we'll obtain multi-sig keys from both Alice and Bob which + // simulates them exchanging keys on a higher level. + aliceFundingKey, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig) + if err != nil { + t.Fatalf("unable to obtain alice funding key: %v", err) + } + bobFundingKey, err := bob.DeriveNextKey(keychain.KeyFamilyMultiSig) + if err != nil { + t.Fatalf("unable to obtain bob funding key: %v", err) + } + + // We'll now set up for them to open a 4 BTC channel, with 1 BTC pushed + // to Bob's side. + chanAmt := 4 * btcutil.SatoshiPerBitcoin + + // Simulating external funding negotiation, we'll now create the + // funding transaction for both parties. Utilizing existing tools, + // we'll create a new chanfunding.Assembler hacked by Alice's wallet. + aliceChanFunder := chanfunding.NewWalletAssembler(chanfunding.WalletConfig{ + CoinSource: lnwallet.NewCoinSource(alice), + CoinSelectLocker: alice, + CoinLocker: alice, + Signer: alice.Cfg.Signer, + DustLimit: 600, + }) + + // With the chan funder created, we'll now provision a funding intent, + // bind the keys we obtained above, and finally obtain our funding + // transaction and outpoint. + fundingIntent, err := aliceChanFunder.ProvisionChannel(&chanfunding.Request{ + LocalAmt: btcutil.Amount(chanAmt), + MinConfs: 1, + FeeRate: 253, + ChangeAddr: func() (btcutil.Address, error) { + return alice.NewAddress(lnwallet.WitnessPubKey, true) + }, + }) + if err != nil { + t.Fatalf("unable to perform coin selection: %v", err) + } + + // With our intent created, we'll instruct it to finalize the funding + // transaction, and also hand us the outpoint so we can simulate + // external crafting of the funding transaction. + var ( + fundingTx *wire.MsgTx + chanPoint *wire.OutPoint + ) + if fullIntent, ok := fundingIntent.(*chanfunding.FullIntent); ok { + fullIntent.BindKeys(&aliceFundingKey, bobFundingKey.PubKey) + + fundingTx, err = fullIntent.CompileFundingTx(nil, nil) + if err != nil { + t.Fatalf("unable to compile funding tx: %v", err) + } + chanPoint, err = fullIntent.ChanPoint() + if err != nil { + t.Fatalf("unable to obtain chan point: %v", err) + } + } else { + t.Fatalf("expected full intent, instead got: %T", fullIntent) + } + + // Now that we have the fully constructed funding transaction, we'll + // create a new shim external funder out of it for Alice, and prep a + // shim intent for Bob. + aliceExternalFunder := chanfunding.NewCannedAssembler( + *chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey, + bobFundingKey.PubKey, true, + ) + bobShimIntent, err := chanfunding.NewCannedAssembler( + *chanPoint, btcutil.Amount(chanAmt), &bobFundingKey, + aliceFundingKey.PubKey, false, + ).ProvisionChannel(nil) + if err != nil { + t.Fatalf("unable to create shim intent for bob: %v", err) + } + + // At this point, we have everything we need to carry out our test, so + // we'll being the funding flow between Alice and Bob. + // + // However, before we do so, we'll register a new shim intent for Bob, + // so he knows what keys to use when he receives the funding request + // from Alice. + pendingChanID := testHdSeed + err = bob.RegisterFundingIntent(pendingChanID, bobShimIntent) + if err != nil { + t.Fatalf("unable to register intent: %v", err) + } + + // Now we can carry out the single funding flow as normal, we'll + // specify our external funder and funding transaction, as well as the + // pending channel ID generated above to allow Alice and Bob to track + // the funding flow externally. + testSingleFunderReservationWorkflow( + miner, alice, bob, t, true, aliceExternalFunder, + func() *wire.MsgTx { + return fundingTx + }, pendingChanID, + ) +} + // TestInterfaces tests all registered interfaces with a unified set of tests // which exercise each of the required methods found within the WalletController // interface. From 6b729ec9f54b5159d0af69afed8455c6651c7ffd Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Oct 2019 21:48:06 -0700 Subject: [PATCH 11/12] lnwallet: break early in TestLightningWallet when a sub-test fails --- lnwallet/interface_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 2622f56bca..53b77800bd 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -2871,8 +2871,10 @@ func TestLightningWallet(t *testing.T) { for _, walletDriver := range lnwallet.RegisteredWallets() { for _, backEnd := range walletDriver.BackEnds() { - runTests(t, walletDriver, backEnd, miningNode, - rpcConfig, chainNotifier) + if !runTests(t, walletDriver, backEnd, miningNode, + rpcConfig, chainNotifier) { + return + } } } } @@ -2884,7 +2886,7 @@ func TestLightningWallet(t *testing.T) { func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, backEnd string, miningNode *rpctest.Harness, rpcConfig rpcclient.ConnConfig, - chainNotifier *btcdnotify.BtcdNotifier) { + chainNotifier chainntnfs.ChainNotifier) bool { var ( bio lnwallet.BlockChainIO @@ -3113,8 +3115,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, bob, err := createTestWallet( tempTestDirBob, miningNode, netParams, - chainNotifier, bobWalletController, bobKeyRing, - bobSigner, bio, + chainNotifier, bobWalletController, bobKeyRing, bobSigner, bio, ) if err != nil { t.Fatalf("unable to create test ln wallet: %v", err) @@ -3135,7 +3136,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, walletTest.test(miningNode, alice, bob, t) }) if !success { - break + return false } // TODO(roasbeef): possible reset mining @@ -3146,4 +3147,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, t.Fatalf("unable to wipe wallet state: %v", err) } } + + return true } From b1940d677907d954ee95e5482b71c33e755cfeb6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 20 Nov 2019 19:54:47 -0800 Subject: [PATCH 12/12] lnwallet: fix bug in verifyFundingInputs skip dual funder tests for neutrino In this commit, we fix a long standing bug within the newly created `verifyFundingInputs` method. Before this commit, the method would attempt to derive the pkScript by looking at the last items on the witness stack, and making a p2wsh output script from that. This is incorrect as typically non of these scripts will actually be p2wsh, and instead will be p2wkh. We fix this by using the newly available `txscript.ComputePkScript` method to derive the proper pkScript. This resolves an issue w.r.t passing incorrect arguments for all backends, but an issue still stands for the neutrino backend. As is, we pass a height hint of zero into the `GetUtxo` method call. With the way the current utxo scanner is set up for neutrino, this'll cause it to never find the UTXO, as it takes the height hint as a UTXO birth height, rather than a lower bound of the birth of the UTXO. --- lnwallet/interface_test.go | 9 +++++++++ lnwallet/wallet.go | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lnwallet/interface_test.go b/lnwallet/interface_test.go index 53b77800bd..989e701756 100644 --- a/lnwallet/interface_test.go +++ b/lnwallet/interface_test.go @@ -3130,9 +3130,18 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, // Execute every test, clearing possibly mutated // wallet state after each step. for _, walletTest := range walletTests { + + walletTest := walletTest + testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd, walletTest.name) success := t.Run(testName, func(t *testing.T) { + if backEnd == "neutrino" && + strings.Contains(walletTest.name, "dual funder") { + t.Skip("skipping dual funder tests for neutrino") + } + return + walletTest.test(miningNode, alice, bob, t) }) if !success { diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 55ef712359..16dc50f95a 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1057,16 +1057,19 @@ func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx, // // TODO(roasbeef): when dual funder pass actual // height-hint - pkScript, err := input.WitnessScriptHash( - txin.Witness[len(txin.Witness)-1], + // + // TODO(roasbeef): this fails for neutrino always as it + // treats the height hint as an exact birthday of the + // utxo rather than a lower bound + pkScript, err := txscript.ComputePkScript( + txin.SignatureScript, txin.Witness, ) if err != nil { return fmt.Errorf("cannot create script: %v", err) } - output, err := l.Cfg.ChainIO.GetUtxo( &txin.PreviousOutPoint, - pkScript, 0, l.quit, + pkScript.Script(), 0, l.quit, ) if output == nil { return fmt.Errorf("input to funding tx does "+