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/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/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)