Merge pull request #3659 from Roasbeef/external-funding-chanfunding

chanfunding: create new package to abstract over funding workflows
This commit is contained in:
Olaoluwa Osuntokun 2019-12-02 18:13:38 -06:00 committed by GitHub
commit f2f79d3990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1614 additions and 595 deletions

@ -147,6 +147,11 @@ const (
// type, but it omits the tweak for one's key in the commitment // type, but it omits the tweak for one's key in the commitment
// transaction of the remote party. // transaction of the remote party.
SingleFunderTweaklessBit ChannelType = 1 << 1 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 // 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 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 // ChannelConstraints represents a set of constraints meant to allow a node to
// limit their exposure, enact flow control and ensure that all HTLCs are // limit their exposure, enact flow control and ensure that all HTLCs are
// economically relevant. This struct will be mirrored for both sides of the // economically relevant. This struct will be mirrored for both sides of the
@ -535,7 +546,9 @@ type OpenChannel struct {
// is found to be pending. // is found to be pending.
// //
// NOTE: This value will only be populated for single-funder channels // 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 FundingTxn *wire.MsgTx
// TODO(roasbeef): eww // 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 { func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
var w bytes.Buffer var w bytes.Buffer
if err := WriteElements(&w, if err := WriteElements(&w,
@ -2535,10 +2558,9 @@ func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
return err return err
} }
// For single funder channels that we initiated, write the funding txn. // For single funder channels that we initiated, and we have the
if channel.ChanType.IsSingleFunder() && channel.IsInitiator && // funding transaction, then write the funding txn.
!channel.hasChanStatus(ChanStatusRestored) { if fundingTxPresent(channel) {
if err := WriteElement(&w, channel.FundingTxn); err != nil { if err := WriteElement(&w, channel.FundingTxn); err != nil {
return err return err
} }
@ -2657,10 +2679,9 @@ func fetchChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
return err return err
} }
// For single funder channels that we initiated, read the funding txn. // For single funder channels that we initiated and have the funding
if channel.ChanType.IsSingleFunder() && channel.IsInitiator && // transaction to, read the funding txn.
!channel.hasChanStatus(ChanStatusRestored) { if fundingTxPresent(channel) {
if err := ReadElement(r, &channel.FundingTxn); err != nil { if err := ReadElement(r, &channel.FundingTxn); err != nil {
return err return err
} }

@ -521,8 +521,9 @@ func (f *fundingManager) start() error {
// Rebroadcast the funding transaction for any pending // Rebroadcast the funding transaction for any pending
// channel that we initiated. No error will be returned // channel that we initiated. No error will be returned
// if the transaction already has been broadcasted. // if the transaction already has been broadcast.
if channel.ChanType.IsSingleFunder() && chanType := channel.ChanType
if chanType.IsSingleFunder() && chanType.HasFundingTx() &&
channel.IsInitiator { channel.IsInitiator {
err := f.cfg.PublishTransaction( err := f.cfg.PublishTransaction(
@ -1215,6 +1216,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
chainHash := chainhash.Hash(msg.ChainHash) chainHash := chainhash.Hash(msg.ChainHash)
req := &lnwallet.InitFundingReserveMsg{ req := &lnwallet.InitFundingReserveMsg{
ChainHash: &chainHash, ChainHash: &chainHash,
PendingChanID: msg.PendingChannelID,
NodeID: fmsg.peer.IdentityKey(), NodeID: fmsg.peer.IdentityKey(),
NodeAddr: fmsg.peer.Address(), NodeAddr: fmsg.peer.Address(),
LocalFundingAmt: 0, LocalFundingAmt: 0,
@ -1739,21 +1741,28 @@ func (f *fundingManager) handleFundingSigned(fmsg *fundingSignedMsg) {
// delete it from our set of active reservations. // delete it from our set of active reservations.
f.deleteReservationCtx(peerKey, pendingChanID) f.deleteReservationCtx(peerKey, pendingChanID)
// Broadcast the finalized funding transaction to the network. // Broadcast the finalized funding transaction to the network, but only
// if we actually have the funding transaction.
if completeChan.ChanType.HasFundingTx() {
fundingTx := completeChan.FundingTxn fundingTx := completeChan.FundingTxn
fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v", fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v",
completeChan.FundingOutpoint, spew.Sdump(fundingTx)) completeChan.FundingOutpoint, spew.Sdump(fundingTx))
err = f.cfg.PublishTransaction(fundingTx) err = f.cfg.PublishTransaction(fundingTx)
if err != nil { if err != nil {
fndgLog.Errorf("Unable to broadcast funding tx for "+ fndgLog.Errorf("Unable to broadcast funding tx for "+
"ChannelPoint(%v): %v", completeChan.FundingOutpoint, "ChannelPoint(%v): %v",
err) completeChan.FundingOutpoint, err)
// We failed to broadcast the funding transaction, but watch
// the channel regardless, in case the transaction made it to // We failed to broadcast the funding transaction, but
// the network. We will retry broadcast at startup. // watch the channel regardless, in case the
// TODO(halseth): retry more often? Handle with CPFP? Just // transaction made it to the network. We will retry
// delete from the DB? // 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, // Now that we have a finalized reservation for this funding flow,
@ -2773,6 +2782,10 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
channelFlags = lnwire.FFAnnounceChannel 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 // Initialize a funding reservation with the local wallet. If the
// wallet doesn't have enough funds to commit to this channel, then the // wallet doesn't have enough funds to commit to this channel, then the
// request will fail, and be aborted. // request will fail, and be aborted.
@ -2790,6 +2803,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
tweaklessCommitment := localTweakless && remoteTweakless tweaklessCommitment := localTweakless && remoteTweakless
req := &lnwallet.InitFundingReserveMsg{ req := &lnwallet.InitFundingReserveMsg{
ChainHash: &msg.chainHash, ChainHash: &msg.chainHash,
PendingChanID: chanID,
NodeID: peerKey, NodeID: peerKey,
NodeAddr: msg.peer.Address(), NodeAddr: msg.peer.Address(),
SubtractFees: msg.subtractFees, SubtractFees: msg.subtractFees,
@ -2815,11 +2829,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
// SubtractFees=true. // SubtractFees=true.
capacity := reservation.Capacity() capacity := reservation.Capacity()
// Obtain a new pending channel ID which is used to track this fndgLog.Infof("Target commit tx sat/kw for pendingID(%x): %v", chanID,
// reservation throughout its lifetime.
chanID := f.nextPendingChanID()
fndgLog.Infof("Target commit tx sat/kw for pending_id(%x): %v", chanID,
int64(commitFeePerKw)) int64(commitFeePerKw))
// If the remote CSV delay was not set in the open channel request, // If the remote CSV delay was not set in the open channel request,

@ -2864,7 +2864,7 @@ func TestFundingManagerFundAll(t *testing.T) {
Value: btcutil.Amount( Value: btcutil.Amount(
0.05 * btcutil.SatoshiPerBitcoin, 0.05 * btcutil.SatoshiPerBitcoin,
), ),
PkScript: make([]byte, 22), PkScript: coinPkScript,
OutPoint: wire.OutPoint{ OutPoint: wire.OutPoint{
Hash: chainhash.Hash{}, Hash: chainhash.Hash{},
Index: 0, Index: 0,
@ -2875,7 +2875,7 @@ func TestFundingManagerFundAll(t *testing.T) {
Value: btcutil.Amount( Value: btcutil.Amount(
0.06 * btcutil.SatoshiPerBitcoin, 0.06 * btcutil.SatoshiPerBitcoin,
), ),
PkScript: make([]byte, 22), PkScript: coinPkScript,
OutPoint: wire.OutPoint{ OutPoint: wire.OutPoint{
Hash: chainhash.Hash{}, Hash: chainhash.Hash{},
Index: 1, Index: 1,

@ -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()
}

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

@ -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
}

@ -1,13 +1,21 @@
package lnwallet package chanfunding
import ( import (
"encoding/hex"
"testing" "testing"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "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 // 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 // with the given number of inputs and the optional change output. This matches
// the estimate done by the wallet. // the estimate done by the wallet.
@ -48,7 +56,7 @@ func TestCoinSelect(t *testing.T) {
type testCase struct { type testCase struct {
name string name string
outputValue btcutil.Amount outputValue btcutil.Amount
coins []*Utxo coins []Coin
expectedInput []btcutil.Amount expectedInput []btcutil.Amount
expectedChange btcutil.Amount expectedChange btcutil.Amount
@ -61,12 +69,14 @@ func TestCoinSelect(t *testing.T) {
// This will obviously lead to a change output of // This will obviously lead to a change output of
// almost 0.5 BTC. // almost 0.5 BTC.
name: "big change", name: "big change",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
outputValue: 0.5 * btcutil.SatoshiPerBitcoin, outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
// The one and only input will be selected. // The one and only input will be selected.
@ -81,12 +91,14 @@ func TestCoinSelect(t *testing.T) {
// This should lead to an error, as we don't have // This should lead to an error, as we don't have
// enough funds to pay the fee. // enough funds to pay the fee.
name: "nothing left for fees", name: "nothing left for fees",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
outputValue: 1 * btcutil.SatoshiPerBitcoin, outputValue: 1 * btcutil.SatoshiPerBitcoin,
expectErr: true, expectErr: true,
}, },
@ -95,12 +107,14 @@ func TestCoinSelect(t *testing.T) {
// as big as possible, such that the remaining change // as big as possible, such that the remaining change
// will be dust. // will be dust.
name: "dust change", name: "dust change",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
// We tune the output value by subtracting the expected // We tune the output value by subtracting the expected
// fee and a small dust amount. // fee and a small dust amount.
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust, outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
@ -117,12 +131,14 @@ func TestCoinSelect(t *testing.T) {
// as big as possible, such that there is nothing left // as big as possible, such that there is nothing left
// for change. // for change.
name: "no change", name: "no change",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
// We tune the output value to be the maximum amount // We tune the output value to be the maximum amount
// possible, leaving just enough for fees. // possible, leaving just enough for fees.
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true), outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
@ -143,7 +159,7 @@ func TestCoinSelect(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
t.Parallel() t.Parallel()
selected, changeAmt, err := coinSelect( selected, changeAmt, err := CoinSelect(
feeRate, test.outputValue, test.coins, feeRate, test.outputValue, test.coins,
) )
if !test.expectErr && err != nil { if !test.expectErr && err != nil {
@ -166,7 +182,7 @@ func TestCoinSelect(t *testing.T) {
} }
for i, coin := range selected { 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, "+ t.Fatalf("expected input %v to have value %v, "+
"had %v", i, test.expectedInput[i], "had %v", i, test.expectedInput[i],
coin.Value) coin.Value)
@ -195,7 +211,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
type testCase struct { type testCase struct {
name string name string
spendValue btcutil.Amount spendValue btcutil.Amount
coins []*Utxo coins []Coin
expectedInput []btcutil.Amount expectedInput []btcutil.Amount
expectedFundingAmt btcutil.Amount expectedFundingAmt btcutil.Amount
@ -209,12 +225,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// should lead to a funding TX with one output, the // should lead to a funding TX with one output, the
// rest goes to fees. // rest goes to fees.
name: "spend all", name: "spend all",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
spendValue: 1 * btcutil.SatoshiPerBitcoin, spendValue: 1 * btcutil.SatoshiPerBitcoin,
// The one and only input will be selected. // The one and only input will be selected.
@ -228,10 +246,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// The total funds available is below the dust limit // The total funds available is below the dust limit
// after paying fees. // after paying fees.
name: "dust output", name: "dust output",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
Value: fundingFee(feeRate, 1, false) + dust, PkScript: p2wkhScript,
Value: int64(fundingFee(feeRate, 1, false) + dust),
},
}, },
}, },
spendValue: fundingFee(feeRate, 1, false) + dust, spendValue: fundingFee(feeRate, 1, false) + dust,
@ -243,12 +263,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
// is below the dust limit. The remainder should go // is below the dust limit. The remainder should go
// towards the funding output. // towards the funding output.
name: "dust change", name: "dust change",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * btcutil.SatoshiPerBitcoin, Value: 1 * btcutil.SatoshiPerBitcoin,
}, },
}, },
},
spendValue: 1*btcutil.SatoshiPerBitcoin - dust, spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
expectedInput: []btcutil.Amount{ expectedInput: []btcutil.Amount{
@ -260,10 +282,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
{ {
// We got just enough funds to create an output above the dust limit. // We got just enough funds to create an output above the dust limit.
name: "output right above dustlimit", name: "output right above dustlimit",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
Value: fundingFee(feeRate, 1, false) + dustLimit + 1, PkScript: p2wkhScript,
Value: int64(fundingFee(feeRate, 1, false) + dustLimit + 1),
},
}, },
}, },
spendValue: 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 // Amount left is below dust limit after paying fee for
// a change output, resulting in a no-change tx. // a change output, resulting in a no-change tx.
name: "no amount to pay fee for change", name: "no amount to pay fee for change",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
Value: fundingFee(feeRate, 1, false) + 2*(dustLimit+1), PkScript: p2wkhScript,
Value: int64(fundingFee(feeRate, 1, false) + 2*(dustLimit+1)),
},
}, },
}, },
spendValue: fundingFee(feeRate, 1, false) + 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. // If more than 20% of funds goes to fees, it should fail.
name: "high fee", name: "high fee",
coins: []*Utxo{ coins: []Coin{
{ {
AddressType: WitnessPubKey, TxOut: wire.TxOut{
Value: 5 * fundingFee(feeRate, 1, false), PkScript: p2wkhScript,
Value: int64(5 * fundingFee(feeRate, 1, false)),
},
}, },
}, },
spendValue: 5 * fundingFee(feeRate, 1, false), spendValue: 5 * fundingFee(feeRate, 1, false),
@ -308,8 +336,10 @@ func TestCoinSelectSubtractFees(t *testing.T) {
} }
for _, test := range testCases { for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
selected, localFundingAmt, changeAmt, err := coinSelectSubtractFees( selected, localFundingAmt, changeAmt, err := CoinSelectSubtractFees(
feeRate, test.spendValue, dustLimit, test.coins, feeRate, test.spendValue, dustLimit, test.coins,
) )
if !test.expectErr && err != nil { if !test.expectErr && err != nil {
@ -332,7 +362,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
} }
for i, coin := range selected { 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, "+ t.Fatalf("expected input %v to have value %v, "+
"had %v", i, test.expectedInput[i], "had %v", i, test.expectedInput[i],
coin.Value) coin.Value)

@ -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
}

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

@ -41,6 +41,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )
@ -616,7 +617,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness,
if err == nil { if err == nil {
t.Fatalf("not error returned, should fail on coin selection") 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) t.Fatalf("error not coinselect error: %v", err)
} }
if failedReservation != nil { if failedReservation != nil {
@ -655,7 +656,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness,
// Attempt to create another channel with 44 BTC, this should fail. // Attempt to create another channel with 44 BTC, this should fail.
_, err = alice.InitChannelReservation(req) _, 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", t.Fatalf("coin selection succeeded should have insufficient funds: %v",
err) err)
} }
@ -699,7 +700,7 @@ func testCancelNonExistentReservation(miner *rpctest.Harness,
// Create our own reservation, give it some ID. // Create our own reservation, give it some ID.
res, err := lnwallet.NewChannelReservation( res, err := lnwallet.NewChannelReservation(
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed, 10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
lnwire.FFAnnounceChannel, true, lnwire.FFAnnounceChannel, true, nil, [32]byte{},
) )
if err != nil { if err != nil {
t.Fatalf("unable to create res: %v", err) 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, 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 // For this scenario, Alice will be the channel initiator while bob
// will act as the responder to the workflow. // will act as the responder to the workflow.
@ -811,6 +814,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
} }
aliceReq := &lnwallet.InitFundingReserveMsg{ aliceReq := &lnwallet.InitFundingReserveMsg{
ChainHash: chainHash, ChainHash: chainHash,
PendingChanID: pendingChanID,
NodeID: bobPub, NodeID: bobPub,
NodeAddr: bobAddr, NodeAddr: bobAddr,
LocalFundingAmt: fundingAmt, LocalFundingAmt: fundingAmt,
@ -820,6 +824,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
PushMSat: pushAmt, PushMSat: pushAmt,
Flags: lnwire.FFAnnounceChannel, Flags: lnwire.FFAnnounceChannel,
Tweakless: tweakless, Tweakless: tweakless,
ChanFunder: aliceChanFunder,
} }
aliceChanReservation, err := alice.InitChannelReservation(aliceReq) aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
if err != nil { if err != nil {
@ -839,15 +844,20 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("unable to verify constraints: %v", err) 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() aliceContribution := aliceChanReservation.OurContribution()
if fetchFundingTx == nil {
if len(aliceContribution.Inputs) < 1 { if len(aliceContribution.Inputs) < 1 {
t.Fatalf("outputs for funding tx not properly selected, have %v "+ t.Fatalf("outputs for funding tx not properly "+
"outputs should at least 1", len(aliceContribution.Inputs)) "selected, have %v outputs should at least 1",
len(aliceContribution.Inputs))
} }
if len(aliceContribution.ChangeOutputs) != 1 { if len(aliceContribution.ChangeOutputs) != 1 {
t.Fatalf("coin selection failed, should have one change outputs, "+ t.Fatalf("coin selection failed, should have one "+
"instead have: %v", len(aliceContribution.ChangeOutputs)) "change outputs, instead have: %v",
len(aliceContribution.ChangeOutputs))
}
} }
assertContributionInitPopulated(t, aliceContribution) assertContributionInitPopulated(t, aliceContribution)
@ -855,6 +865,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// reservation initiation, then consume Alice's contribution. // reservation initiation, then consume Alice's contribution.
bobReq := &lnwallet.InitFundingReserveMsg{ bobReq := &lnwallet.InitFundingReserveMsg{
ChainHash: chainHash, ChainHash: chainHash,
PendingChanID: pendingChanID,
NodeID: alicePub, NodeID: alicePub,
NodeAddr: aliceAddr, NodeAddr: aliceAddr,
LocalFundingAmt: 0, LocalFundingAmt: 0,
@ -897,10 +908,11 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// At this point, Alice should have generated all the signatures // At this point, Alice should have generated all the signatures
// required for the funding transaction, as well as Alice's commitment // 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() aliceRemoteContribution := aliceChanReservation.TheirContribution()
aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures() aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures()
if aliceFundingSigs == nil { if fetchFundingTx == nil && aliceFundingSigs == nil {
t.Fatalf("funding sigs not found") t.Fatalf("funding sigs not found")
} }
if aliceCommitSig == nil { if aliceCommitSig == nil {
@ -909,7 +921,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// Additionally, the funding tx and the funding outpoint should have // Additionally, the funding tx and the funding outpoint should have
// been populated. // been populated.
if aliceChanReservation.FinalFundingTx() == nil { if aliceChanReservation.FinalFundingTx() == nil && fetchFundingTx == nil {
t.Fatalf("funding transaction never created!") t.Fatalf("funding transaction never created!")
} }
if aliceChanReservation.FundingOutpoint() == nil { if aliceChanReservation.FundingOutpoint() == nil {
@ -951,9 +963,17 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("alice unable to complete reservation: %v", err) 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 // The resulting active channel state should have been persisted to the
// DB for both Alice and Bob. // DB for both Alice and Bob.
fundingTx := aliceChanReservation.FinalFundingTx()
fundingSha := fundingTx.TxHash() fundingSha := fundingTx.TxHash()
aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub) aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub)
if err != nil { if err != nil {
@ -2523,7 +2543,8 @@ var walletTests = []walletTestCase{
bob *lnwallet.LightningWallet, t *testing.T) { bob *lnwallet.LightningWallet, t *testing.T) {
testSingleFunderReservationWorkflow( 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) { bob *lnwallet.LightningWallet, t *testing.T) {
testSingleFunderReservationWorkflow( 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", name: "dual funder workflow",
test: testDualFundingReservationWorkflow, test: testDualFundingReservationWorkflow,
@ -2675,6 +2701,114 @@ func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error {
return nil 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 // TestInterfaces tests all registered interfaces with a unified set of tests
// which exercise each of the required methods found within the WalletController // which exercise each of the required methods found within the WalletController
// interface. // interface.
@ -2737,8 +2871,10 @@ func TestLightningWallet(t *testing.T) {
for _, walletDriver := range lnwallet.RegisteredWallets() { for _, walletDriver := range lnwallet.RegisteredWallets() {
for _, backEnd := range walletDriver.BackEnds() { for _, backEnd := range walletDriver.BackEnds() {
runTests(t, walletDriver, backEnd, miningNode, if !runTests(t, walletDriver, backEnd, miningNode,
rpcConfig, chainNotifier) rpcConfig, chainNotifier) {
return
}
} }
} }
} }
@ -2750,7 +2886,7 @@ func TestLightningWallet(t *testing.T) {
func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver, func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
backEnd string, miningNode *rpctest.Harness, backEnd string, miningNode *rpctest.Harness,
rpcConfig rpcclient.ConnConfig, rpcConfig rpcclient.ConnConfig,
chainNotifier *btcdnotify.BtcdNotifier) { chainNotifier chainntnfs.ChainNotifier) bool {
var ( var (
bio lnwallet.BlockChainIO bio lnwallet.BlockChainIO
@ -2979,8 +3115,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
bob, err := createTestWallet( bob, err := createTestWallet(
tempTestDirBob, miningNode, netParams, tempTestDirBob, miningNode, netParams,
chainNotifier, bobWalletController, bobKeyRing, chainNotifier, bobWalletController, bobKeyRing, bobSigner, bio,
bobSigner, bio,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err) 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 // Execute every test, clearing possibly mutated
// wallet state after each step. // wallet state after each step.
for _, walletTest := range walletTests { for _, walletTest := range walletTests {
walletTest := walletTest
testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd, testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd,
walletTest.name) walletTest.name)
success := t.Run(testName, func(t *testing.T) { 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) walletTest.test(miningNode, alice, bob, t)
}) })
if !success { if !success {
break return false
} }
// TODO(roasbeef): possible reset mining // 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) t.Fatalf("unable to wipe wallet state: %v", err)
} }
} }
return true
} }

@ -11,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )
@ -109,19 +110,19 @@ type ChannelReservation struct {
// throughout its lifetime. // throughout its lifetime.
reservationID uint64 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 // pushMSat the amount of milli-satoshis that should be pushed to the
// responder of a single funding channel as part of the initial // responder of a single funding channel as part of the initial
// commitment state. // commitment state.
pushMSat lnwire.MilliSatoshi 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 wallet *LightningWallet
chanFunder chanfunding.Assembler
fundingIntent chanfunding.Intent
} }
// NewChannelReservation creates a new channel reservation. This function is // NewChannelReservation creates a new channel reservation. This function is
@ -131,8 +132,9 @@ type ChannelReservation struct {
func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet, commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet,
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash, id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
flags lnwire.FundingFlag, flags lnwire.FundingFlag, tweaklessCommit bool,
tweaklessCommit bool) (*ChannelReservation, error) { fundingAssembler chanfunding.Assembler,
pendingChanID [32]byte) (*ChannelReservation, error) {
var ( var (
ourBalance lnwire.MilliSatoshi ourBalance lnwire.MilliSatoshi
@ -220,6 +222,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
} else { } else {
chanType |= channeldb.SingleFunderBit 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 { } else {
// Otherwise, this is a dual funder channel, and no side is // Otherwise, this is a dual funder channel, and no side is
// technically the "initiator" // technically the "initiator"
@ -258,10 +268,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
Db: wallet.Cfg.Database, Db: wallet.Cfg.Database,
}, },
pushMSat: pushMSat, pushMSat: pushMSat,
pendingChanID: pendingChanID,
reservationID: id, reservationID: id,
chanOpen: make(chan *openChanDetails, 1),
chanOpenErr: make(chan error, 1),
wallet: wallet, wallet: wallet,
chanFunder: fundingAssembler,
}, nil }, nil
} }

@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"math"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -22,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate" "github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain" "github.com/lightningnetwork/lnd/shachain"
@ -33,20 +33,6 @@ const (
msgBufferSize = 100 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 // InitFundingReserveMsg is the first message sent to initiate the workflow
// required to open a payment channel with a remote peer. The initial required // required to open a payment channel with a remote peer. The initial required
// parameters are configurable across channels. These parameters are to be // parameters are configurable across channels. These parameters are to be
@ -62,6 +48,10 @@ type InitFundingReserveMsg struct {
// target channel. // target channel.
ChainHash *chainhash.Hash 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 // NodeID is the ID of the remote node we would like to open a channel
// with. // with.
NodeID *btcec.PublicKey NodeID *btcec.PublicKey
@ -112,6 +102,12 @@ type InitFundingReserveMsg struct {
// commitment format or not. // commitment format or not.
Tweakless bool 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 // err is a channel in which all errors will be sent across. Will be
// nil if this initial set is successful. // nil if this initial set is successful.
// //
@ -281,6 +277,11 @@ type LightningWallet struct {
// the currently locked outpoints. // the currently locked outpoints.
lockedOutPoints map[wire.OutPoint]struct{} 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{} quit chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
@ -301,6 +302,7 @@ func NewLightningWallet(Cfg Config) (*LightningWallet, error) {
nextFundingID: 0, nextFundingID: 0,
fundingLimbo: make(map[uint64]*ChannelReservation), fundingLimbo: make(map[uint64]*ChannelReservation),
lockedOutPoints: make(map[wire.OutPoint]struct{}), lockedOutPoints: make(map[wire.OutPoint]struct{}),
fundingIntents: make(map[[32]byte]chanfunding.Intent),
quit: make(chan struct{}), quit: make(chan struct{}),
}, nil }, nil
} }
@ -439,6 +441,21 @@ func (l *LightningWallet) InitChannelReservation(
return <-req.resp, <-req.err 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 // handleFundingReserveRequest processes a message intending to create, and
// validate a funding reservation request. // validate a funding reservation request.
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) { func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
@ -461,24 +478,55 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return 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 localFundingAmt := req.LocalFundingAmt
remoteFundingAmt := req.RemoteFundingAmt
var ( var (
selected *coinSelection fundingIntent chanfunding.Intent
err error err error
) )
// If we're on the receiving end of a single funder channel then we // If we've just received an inbound funding request that we have a
// don't need to perform any coin selection, and the remote contributes // registered shim intent to, then we'll obtain the backing intent now.
// all funds. Otherwise, attempt to obtain enough coins to meet the // In this case, we're doing a special funding workflow that allows
// required funding amount. // more advanced constructions such as channel factories to be
if req.LocalFundingAmt != 0 { // 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 // Coin selection is done on the basis of sat/kw, so we'll use
// the fee rate passed in to perform coin selection. // the fee rate passed in to perform coin selection.
var err error var err error
selected, err = l.selectCoinsAndChange( fundingReq := &chanfunding.Request{
req.FundingFeePerKw, req.LocalFundingAmt, req.MinConfs, RemoteAmt: req.RemoteFundingAmt,
req.SubtractFees, 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 { if err != nil {
req.err <- err req.err <- err
@ -486,31 +534,51 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return return
} }
localFundingAmt = selected.fundingAmt localFundingAmt = fundingIntent.LocalFundingAmt()
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
} }
// The total channel capacity will be the size of the funding output we // The total channel capacity will be the size of the funding output we
// created plus the remote contribution. // created plus the remote contribution.
capacity := localFundingAmt + req.RemoteFundingAmt capacity := localFundingAmt + remoteFundingAmt
id := atomic.AddUint64(&l.nextFundingID, 1) id := atomic.AddUint64(&l.nextFundingID, 1)
reservation, err := NewChannelReservation( reservation, err := NewChannelReservation(
capacity, localFundingAmt, req.CommitFeePerKw, l, id, capacity, localFundingAmt, req.CommitFeePerKw, l, id,
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags, req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
req.Tweakless, req.Tweakless, req.ChanFunder, req.PendingChanID,
) )
if err != nil { if err != nil {
selected.unlockCoins() if fundingIntent != nil {
fundingIntent.Cancel()
}
req.err <- err req.err <- err
req.resp <- nil req.resp <- nil
return 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( err = l.initOurContribution(
reservation, selected, req.NodeAddr, req.NodeID, reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
) )
if err != nil { if err != nil {
selected.unlockCoins() if fundingIntent != nil {
fundingIntent.Cancel()
}
req.err <- err req.err <- err
req.resp <- nil req.resp <- nil
return 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 // and change reserved for the channel, and derives the keys to use for this
// channel. // channel.
func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, 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 // Grab the mutex on the ChannelReservation to ensure thread-safety
reservation.Lock() reservation.Lock()
defer reservation.Unlock() defer reservation.Unlock()
if selected != nil { // At this point, if we have a funding intent, we'll use it to populate
reservation.ourContribution.Inputs = selected.coins // the existing reservation state entries for our coin selection.
reservation.ourContribution.ChangeOutputs = selected.change 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.nodeAddr = nodeAddr
reservation.partialState.IdentityPub = nodeID 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 var err error
reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey( reservation.ourContribution.MultiSigKey, err = keyRing.DeriveNextKey(
keychain.KeyFamilyMultiSig, keychain.KeyFamilyMultiSig,
) )
if err != nil { if err != nil {
return err return err
} }
reservation.ourContribution.RevocationBasePoint, err = l.DeriveNextKey( reservation.ourContribution.RevocationBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationBase, keychain.KeyFamilyRevocationBase,
) )
if err != nil { if err != nil {
return err return err
} }
reservation.ourContribution.HtlcBasePoint, err = l.DeriveNextKey( reservation.ourContribution.HtlcBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyHtlcBase, keychain.KeyFamilyHtlcBase,
) )
if err != nil { if err != nil {
return err return err
} }
reservation.ourContribution.PaymentBasePoint, err = l.DeriveNextKey( reservation.ourContribution.PaymentBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyPaymentBase, keychain.KeyFamilyPaymentBase,
) )
if err != nil { if err != nil {
return err return err
} }
reservation.ourContribution.DelayBasePoint, err = l.DeriveNextKey( reservation.ourContribution.DelayBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyDelayBase, keychain.KeyFamilyDelayBase,
) )
if err != nil { 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 // With the above keys created, we'll also need to initialization our
// initial revocation tree state. // initial revocation tree state.
nextRevocationKeyDesc, err := l.DeriveNextKey( nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot, keychain.KeyFamilyRevocationRoot,
) )
if err != nil { if err != nil {
@ -653,6 +729,16 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
delete(l.fundingLimbo, req.pendingFundingID) 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 req.err <- nil
} }
@ -722,100 +808,82 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
pendingReservation.Lock() pendingReservation.Lock()
defer pendingReservation.Unlock() 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. // Some temporary variables to cut down on the resolution verbosity.
pendingReservation.theirContribution = req.contribution pendingReservation.theirContribution = req.contribution
theirContribution := req.contribution theirContribution := req.contribution
ourContribution := pendingReservation.ourContribution ourContribution := pendingReservation.ourContribution
// Add all multi-party inputs and outputs to the transaction. var (
for _, ourInput := range ourContribution.Inputs { chanPoint *wire.OutPoint
fundingTx.AddTxIn(ourInput) err error
}
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,
) )
// 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 { if err != nil {
req.err <- err req.err <- fmt.Errorf("unable to obtain chan point: %v", err)
return return
} }
// Sort the transaction. Since both side agree to a canonical ordering, pendingReservation.partialState.FundingOutpoint = *chanPoint
// 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 case *chanfunding.FullIntent:
// order of the inputs. // Now that we know their public key, we can bind theirs as
pendingReservation.ourFundingInputScripts = make([]*input.Script, 0, // well as ours to the funding intent.
len(ourContribution.Inputs)) fundingIntent.BindKeys(
signDesc := input.SignDescriptor{ &pendingReservation.ourContribution.MultiSigKey,
HashType: txscript.SigHashAll, theirContribution.MultiSigKey.PubKey,
SigHashes: txscript.NewTxSigHashes(fundingTx), )
// With our keys bound, we can now construct+sign the final
// funding transaction and also obtain the chanPoint that
// creates the channel.
fundingTx, err := fundingIntent.CompileFundingTx(
theirContribution.Inputs,
theirContribution.ChangeOutputs,
)
if err != nil {
req.err <- fmt.Errorf("unable to construct funding "+
"tx: %v", err)
return
} }
for i, txIn := range fundingTx.TxIn { chanPoint, err = fundingIntent.ChanPoint()
info, err := l.FetchInputInfo(&txIn.PreviousOutPoint) if err != nil {
if err == ErrNotMine { req.err <- fmt.Errorf("unable to obtain chan "+
"point: %v", err)
return
}
// 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 continue
} else if err != nil {
req.err <- err
return
} }
signDesc.Output = &wire.TxOut{
PkScript: info.PkScript,
Value: int64(info.Value),
}
signDesc.InputIndex = i
inputScript, err := l.Cfg.Signer.ComputeInputScript(
fundingTx, &signDesc,
)
if err != nil {
req.err <- err
return
}
txIn.SignatureScript = inputScript.SigScript
txIn.Witness = inputScript.Witness
pendingReservation.ourFundingInputScripts = append( pendingReservation.ourFundingInputScripts = append(
pendingReservation.ourFundingInputScripts, pendingReservation.ourFundingInputScripts,
inputScript, &input.Script{
Witness: txIn.Witness,
SigScript: txIn.SignatureScript,
},
) )
} }
// Locate the index of the multi-sig outpoint in order to record it walletLog.Debugf("Funding tx for ChannelPoint(%v) "+
// since the outputs are canonically sorted. If this is a single funder "generated: %v", chanPoint, spew.Sdump(fundingTx))
// 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 // 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 // revocation hash (we don't yet know the preimage so we can't add it
@ -832,10 +900,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
// Create the txin to our commitment transaction; required to construct // Create the txin to our commitment transaction; required to construct
// the commitment transactions. // the commitment transactions.
fundingTxIn := wire.TxIn{ fundingTxIn := wire.TxIn{
PreviousOutPoint: wire.OutPoint{ PreviousOutPoint: *chanPoint,
Hash: fundingTxID,
Index: multiSigIndex,
},
} }
// With the funding tx complete, create both commitment transactions. // With the funding tx complete, create both commitment transactions.
@ -892,21 +957,32 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
txsort.InPlaceSort(theirCommitTx) txsort.InPlaceSort(theirCommitTx)
walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v", 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", 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. // Record newly available information within the open channel state.
chanState.FundingOutpoint = *fundingOutpoint chanState.FundingOutpoint = *chanPoint
chanState.LocalCommitment.CommitTx = ourCommitTx chanState.LocalCommitment.CommitTx = ourCommitTx
chanState.RemoteCommitment.CommitTx = theirCommitTx 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 // Generate a signature for their version of the initial commitment
// transaction. // transaction.
signDesc = input.SignDescriptor{ ourKey := ourContribution.MultiSigKey
WitnessScript: witnessScript, signDesc := input.SignDescriptor{
WitnessScript: fundingWitnessScript,
KeyDesc: ourKey, KeyDesc: ourKey,
Output: multiSigOut, Output: fundingOutput,
HashType: txscript.SigHashAll, HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(theirCommitTx), SigHashes: txscript.NewTxSigHashes(theirCommitTx),
InputIndex: 0, InputIndex: 0,
@ -962,12 +1038,64 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg
return return
} }
// openChanDetails contains a "finalized" channel which can be considered // verifyFundingInputs attempts to verify all remote inputs to the funding
// "open" according to the requested confirmation depth at reservation // transaction.
// initialization. Additionally, the struct contains additional details func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx,
// pertaining to the exact location in the main chain in-which the transaction remoteInputScripts []*input.Script) error {
// was confirmed.
type openChanDetails struct { 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 // handleFundingCounterPartySigs is the final step in the channel reservation
@ -994,60 +1122,18 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// signatures to their inputs. // signatures to their inputs.
res.theirFundingInputScripts = msg.theirFundingInputScripts res.theirFundingInputScripts = msg.theirFundingInputScripts
inputScripts := 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 fundingTx := res.fundingTx
sigIndex := 0 if res.partialState.ChanType.HasFundingTx() {
fundingHashCache := txscript.NewTxSigHashes(fundingTx) err := l.verifyFundingInputs(fundingTx, inputScripts)
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 { if err != nil {
msg.err <- fmt.Errorf("cannot create script: "+ msg.err <- err
"%v", err)
msg.completeChan <- nil msg.completeChan <- nil
return 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++
}
} }
// At this point, we can also record and verify their signature for our // At this point, we can also record and verify their signature for our
@ -1075,8 +1161,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// is complete, allowing us to spend from the funding transaction. // is complete, allowing us to spend from the funding transaction.
channelValue := int64(res.partialState.Capacity) channelValue := int64(res.partialState.Capacity)
hashCache := txscript.NewTxSigHashes(commitTx) hashCache := txscript.NewTxSigHashes(commitTx)
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, sigHash, err := txscript.CalcWitnessSigHash(
txscript.SigHashAll, commitTx, 0, channelValue) witnessScript, hashCache, txscript.SigHashAll, commitTx,
0, channelValue,
)
if err != nil { if err != nil {
msg.err <- err msg.err <- err
msg.completeChan <- nil msg.completeChan <- nil
@ -1103,6 +1191,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
delete(l.fundingLimbo, res.reservationID) delete(l.fundingLimbo, res.reservationID)
l.limboMtx.Unlock() 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 // As we're about to broadcast the funding transaction, we'll take note
// of the current height for record keeping purposes. // of the current height for record keeping purposes.
// //
@ -1223,8 +1315,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return return
} }
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache, sigHash, err := txscript.CalcWitnessSigHash(
txscript.SigHashAll, ourCommitTx, 0, channelValue) witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0,
channelValue,
)
if err != nil { if err != nil {
req.err <- err req.err <- err
req.completeChan <- nil req.completeChan <- nil
@ -1238,7 +1332,8 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
req.err <- err req.err <- err
req.completeChan <- nil req.completeChan <- nil
return return
} else if !sig.Verify(sigHash, theirKey.PubKey) { }
if !sig.Verify(sigHash, theirKey.PubKey) {
req.err <- fmt.Errorf("counterparty's commitment signature " + req.err <- fmt.Errorf("counterparty's commitment signature " +
"is invalid") "is invalid")
req.completeChan <- nil req.completeChan <- nil
@ -1298,6 +1393,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
l.limboMtx.Lock() l.limboMtx.Lock()
delete(l.fundingLimbo, req.pendingFundingID) delete(l.fundingLimbo, req.pendingFundingID)
l.limboMtx.Unlock() l.limboMtx.Unlock()
l.intentMtx.Lock()
delete(l.fundingIntents, pendingReservation.pendingChanID)
l.intentMtx.Unlock()
} }
// WithCoinSelectLock will execute the passed function closure in a // WithCoinSelectLock will execute the passed function closure in a
@ -1311,127 +1410,6 @@ func (l *LightningWallet) WithCoinSelectLock(f func() error) error {
return f() 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 // 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 // state hints from the root to be used for a new channel. The obfuscator is
// generated via the following computation: // generated via the following computation:
@ -1468,179 +1446,6 @@ func initStateHints(commit1, commit2 *wire.MsgTx,
return nil 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 // ValidateChannel will attempt to fully validate a newly mined channel, given
// its funding transaction and existing channel state. If this method returns // its funding transaction and existing channel state. If this method returns
// an error, then the mined channel is invalid, and shouldn't be used. // 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 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
}

2
log.go

@ -26,6 +26,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/monitoring" "github.com/lightningnetwork/lnd/monitoring"
"github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/netann"
"github.com/lightningnetwork/lnd/peernotifier" "github.com/lightningnetwork/lnd/peernotifier"
@ -96,6 +97,7 @@ func init() {
addSubLogger("PROM", monitoring.UseLogger) addSubLogger("PROM", monitoring.UseLogger)
addSubLogger("WTCL", wtclient.UseLogger) addSubLogger("WTCL", wtclient.UseLogger)
addSubLogger("PRNF", peernotifier.UseLogger) addSubLogger("PRNF", peernotifier.UseLogger)
addSubLogger("CHFD", chanfunding.UseLogger)
addSubLogger(routerrpc.Subsystem, routerrpc.UseLogger) addSubLogger(routerrpc.Subsystem, routerrpc.UseLogger)
addSubLogger(wtclientrpc.Subsystem, wtclientrpc.UseLogger) addSubLogger(wtclientrpc.Subsystem, wtclientrpc.UseLogger)

@ -1,6 +1,7 @@
package lnd package lnd
import ( import (
"encoding/hex"
"fmt" "fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -20,6 +21,10 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
var (
coinPkScript, _ = hex.DecodeString("001431df1bde03c074d0cf21ea2529427e1499b8f1de")
)
// The block height returned by the mock BlockChainIO's GetBestBlock. // The block height returned by the mock BlockChainIO's GetBestBlock.
const fundingBroadcastHeight = 123 const fundingBroadcastHeight = 123
@ -297,7 +302,7 @@ func (m *mockWalletController) ListUnspentWitness(minconfirms,
utxo := &lnwallet.Utxo{ utxo := &lnwallet.Utxo{
AddressType: lnwallet.WitnessPubKey, AddressType: lnwallet.WitnessPubKey,
Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin), Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin),
PkScript: make([]byte, 22), PkScript: coinPkScript,
OutPoint: wire.OutPoint{ OutPoint: wire.OutPoint{
Hash: chainhash.Hash{}, Hash: chainhash.Hash{},
Index: m.index, Index: m.index,