diff --git a/channeldb/channel.go b/channeldb/channel.go index 9332f50e..70722dfa 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 } diff --git a/fundingmanager.go b/fundingmanager.go index 189eda49..e87dd9f1 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( @@ -1215,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, @@ -1739,21 +1741,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, @@ -2773,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. @@ -2790,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, @@ -2815,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/fundingmanager_test.go b/fundingmanager_test.go index 09bbe660..80d3cd33 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/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go new file mode 100644 index 00000000..e320d9ee --- /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 00000000..10fe811b --- /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/coin_select.go b/lnwallet/chanfunding/coin_select.go new file mode 100644 index 00000000..f1ce008d --- /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 903f5e4d..67b24976 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/chanfunding/log.go b/lnwallet/chanfunding/log.go new file mode 100644 index 00000000..159a96ca --- /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 00000000..554654fe --- /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/lnwallet/interface_test.go b/lnwallet/interface_test.go index 8f8e1748..989e7017 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) @@ -792,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. @@ -811,6 +814,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, } aliceReq := &lnwallet.InitFundingReserveMsg{ ChainHash: chainHash, + PendingChanID: pendingChanID, NodeID: bobPub, NodeAddr: bobAddr, LocalFundingAmt: fundingAmt, @@ -820,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 { @@ -839,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) @@ -855,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, @@ -897,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 { @@ -909,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 { @@ -951,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 { @@ -2523,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{}, ) }, }, @@ -2533,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, @@ -2675,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. @@ -2737,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 + } } } } @@ -2750,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 @@ -2979,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) @@ -2995,13 +3130,22 @@ 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 { - break + return false } // TODO(roasbeef): possible reset mining @@ -3012,4 +3156,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, t.Fatalf("unable to wipe wallet state: %v", err) } } + + return true } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 36574bb5..19f49eea 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" ) @@ -109,19 +110,19 @@ 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. 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 + chanFunder chanfunding.Assembler - wallet *LightningWallet + fundingIntent chanfunding.Intent } // NewChannelReservation creates a new channel reservation. This function is @@ -131,8 +132,9 @@ 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, + pendingChanID [32]byte) (*ChannelReservation, error) { var ( ourBalance lnwire.MilliSatoshi @@ -220,6 +222,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" @@ -258,10 +268,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, Db: wallet.Cfg.Database, }, pushMSat: pushMSat, + pendingChanID: pendingChanID, reservationID: id, - chanOpen: make(chan *openChanDetails, 1), - chanOpenErr: make(chan error, 1), wallet: wallet, + chanFunder: fundingAssembler, }, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 342579e9..16dc50f9 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "errors" "fmt" - "math" "net" "sync" "sync/atomic" @@ -22,6 +21,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 +33,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 @@ -62,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 @@ -112,6 +102,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. // @@ -281,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 @@ -301,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 } @@ -439,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) { @@ -461,24 +478,55 @@ 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 - // 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 - 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 @@ -486,31 +534,51 @@ 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, req.PendingChanID, ) if err != nil { - selected.unlockCoins() + if fundingIntent != nil { + fundingIntent.Cancel() + } + req.err <- err req.resp <- nil 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, selected, req.NodeAddr, req.NodeID, + reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing, ) if err != nil { - selected.unlockCoins() + if fundingIntent != nil { + fundingIntent.Cancel() + } + req.err <- err req.resp <- nil return @@ -533,52 +601,60 @@ 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, keyRing keychain.KeyRing) 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( + 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 { @@ -587,7 +663,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 { @@ -653,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 } @@ -722,101 +808,83 @@ 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) - - // 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 + // 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.ShimIntent: + chanPoint, err = fundingIntent.ChanPoint() + if err != nil { + req.err <- fmt.Errorf("unable to obtain chan point: %v", err) return } - signDesc.Output = &wire.TxOut{ - PkScript: info.PkScript, - Value: int64(info.Value), - } - signDesc.InputIndex = i + pendingReservation.partialState.FundingOutpoint = *chanPoint - inputScript, err := l.Cfg.Signer.ComputeInputScript( - fundingTx, &signDesc, + 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, + ) + + // 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 <- err + req.err <- fmt.Errorf("unable to construct funding "+ + "tx: %v", err) + return + } + chanPoint, err = fundingIntent.ChanPoint() + if err != nil { + 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 + } + + pendingReservation.ourFundingInputScripts = append( + pendingReservation.ourFundingInputScripts, + &input.Script{ + Witness: txIn.Witness, + SigScript: txIn.SignatureScript, + }, + ) + } + + walletLog.Debugf("Funding tx for ChannelPoint(%v) "+ + "generated: %v", chanPoint, spew.Sdump(fundingTx)) } - // 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 - - walletLog.Debugf("Funding tx for ChannelPoint(%v) generated: %v", - fundingOutpoint, 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 // to the chain). @@ -832,10 +900,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. @@ -892,21 +957,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, @@ -962,12 +1038,64 @@ 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 { +// verifyFundingInputs attempts to verify all remote inputs to the funding +// transaction. +func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx, + remoteInputScripts []*input.Script) error { + + 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. + txin.Witness = inputScripts[sigIndex].Witness + txin.SignatureScript = inputScripts[sigIndex].SigScript + + // Fetch the alleged previous output along with the + // pkscript referenced by this input. + // + // TODO(roasbeef): when dual funder pass actual + // height-hint + // + // 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.Script(), 0, l.quit, + ) + if output == nil { + 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, + ) + if err != nil { + return fmt.Errorf("cannot create script "+ + "engine: %s", err) + } + if err = vm.Execute(); err != nil { + return fmt.Errorf("cannot validate "+ + "transaction: %s", err) + } + + sigIndex++ + } + } + + return nil } // handleFundingCounterPartySigs is the final step in the channel reservation @@ -994,59 +1122,17 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // 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 - sigIndex := 0 - fundingHashCache := txscript.NewTxSigHashes(fundingTx) - for i, txin := range fundingTx.TxIn { - if len(inputScripts) != 0 && len(txin.Witness) == 0 { - // Attach the input scripts so we can verify it below. - txin.Witness = inputScripts[sigIndex].Witness - txin.SignatureScript = inputScripts[sigIndex].SigScript - - // Fetch the alleged previous output along with the - // pkscript referenced by this input. - // - // TODO(roasbeef): when dual funder pass actual - // height-hint - pkScript, err := input.WitnessScriptHash( - txin.Witness[len(txin.Witness)-1], - ) - if err != nil { - msg.err <- fmt.Errorf("cannot create script: "+ - "%v", err) - msg.completeChan <- nil - return - } - - output, err := l.Cfg.ChainIO.GetUtxo( - &txin.PreviousOutPoint, - pkScript, 0, l.quit, - ) - if output == nil { - msg.err <- fmt.Errorf("input to funding tx "+ - "does not exist: %v", err) - msg.completeChan <- nil - return - } - - // Ensure that the witness+sigScript combo is valid. - vm, err := txscript.NewEngine(output.PkScript, - fundingTx, i, txscript.StandardVerifyFlags, nil, - fundingHashCache, output.Value) - if err != nil { - msg.err <- fmt.Errorf("cannot create script "+ - "engine: %s", err) - msg.completeChan <- nil - return - } - if err = vm.Execute(); err != nil { - msg.err <- fmt.Errorf("cannot validate "+ - "transaction: %s", err) - msg.completeChan <- nil - return - } - - sigIndex++ + if res.partialState.ChanType.HasFundingTx() { + err := l.verifyFundingInputs(fundingTx, inputScripts) + if err != nil { + msg.err <- err + msg.completeChan <- nil + return } } @@ -1075,8 +1161,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 @@ -1103,6 +1191,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. // @@ -1223,8 +1315,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 @@ -1238,7 +1332,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 @@ -1298,6 +1393,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 @@ -1311,127 +1410,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. - coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32) - if err != nil { - return nil, err - } - - var ( - selectedCoins []*Utxo - 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 = 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 = 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: @@ -1468,179 +1446,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. @@ -1695,3 +1500,81 @@ 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 +} + +// 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 +} diff --git a/log.go b/log.go index a6d81f26..0ad511f4 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) diff --git a/mock.go b/mock.go index 609b7818..bf9559d7 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,