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
// transaction of the remote party.
SingleFunderTweaklessBit ChannelType = 1 << 1
// NoFundingTxBit denotes if we have the funding transaction locally on
// disk. This bit may be on if the funding transaction was crafted by a
// wallet external to the primary daemon.
NoFundingTxBit ChannelType = 1 << 2
)
// IsSingleFunder returns true if the channel type if one of the known single
@ -166,6 +171,12 @@ func (c ChannelType) IsTweakless() bool {
return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit
}
// HasFundingTx returns true if this channel type is one that has a funding
// transaction stored locally.
func (c ChannelType) HasFundingTx() bool {
return c&NoFundingTxBit == 0
}
// ChannelConstraints represents a set of constraints meant to allow a node to
// limit their exposure, enact flow control and ensure that all HTLCs are
// economically relevant. This struct will be mirrored for both sides of the
@ -535,7 +546,9 @@ type OpenChannel struct {
// is found to be pending.
//
// NOTE: This value will only be populated for single-funder channels
// for which we are the initiator.
// for which we are the initiator, and that we also have the funding
// transaction for. One can check this by using the HasFundingTx()
// method on the ChanType field.
FundingTxn *wire.MsgTx
// TODO(roasbeef): eww
@ -2522,6 +2535,16 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error {
)
}
// fundingTxPresent returns true if expect the funding transcation to be found
// on disk or already populated within the passed oen chanel struct.
func fundingTxPresent(channel *OpenChannel) bool {
chanType := channel.ChanType
return chanType.IsSingleFunder() && chanType.HasFundingTx() &&
channel.IsInitiator &&
!channel.hasChanStatus(ChanStatusRestored)
}
func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
var w bytes.Buffer
if err := WriteElements(&w,
@ -2535,10 +2558,9 @@ func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
return err
}
// For single funder channels that we initiated, write the funding txn.
if channel.ChanType.IsSingleFunder() && channel.IsInitiator &&
!channel.hasChanStatus(ChanStatusRestored) {
// For single funder channels that we initiated, and we have the
// funding transaction, then write the funding txn.
if fundingTxPresent(channel) {
if err := WriteElement(&w, channel.FundingTxn); err != nil {
return err
}
@ -2657,10 +2679,9 @@ func fetchChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
return err
}
// For single funder channels that we initiated, read the funding txn.
if channel.ChanType.IsSingleFunder() && channel.IsInitiator &&
!channel.hasChanStatus(ChanStatusRestored) {
// For single funder channels that we initiated and have the funding
// transaction to, read the funding txn.
if fundingTxPresent(channel) {
if err := ReadElement(r, &channel.FundingTxn); err != nil {
return err
}

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

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

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

@ -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/btcwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -616,7 +617,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness,
if err == nil {
t.Fatalf("not error returned, should fail on coin selection")
}
if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok {
if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok {
t.Fatalf("error not coinselect error: %v", err)
}
if failedReservation != nil {
@ -655,7 +656,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness,
// Attempt to create another channel with 44 BTC, this should fail.
_, err = alice.InitChannelReservation(req)
if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok {
if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok {
t.Fatalf("coin selection succeeded should have insufficient funds: %v",
err)
}
@ -699,7 +700,7 @@ func testCancelNonExistentReservation(miner *rpctest.Harness,
// Create our own reservation, give it some ID.
res, err := lnwallet.NewChannelReservation(
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
lnwire.FFAnnounceChannel, true,
lnwire.FFAnnounceChannel, true, nil, [32]byte{},
)
if err != nil {
t.Fatalf("unable to create res: %v", err)
@ -792,7 +793,9 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi
}
func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool) {
alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool,
aliceChanFunder chanfunding.Assembler,
fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) {
// For this scenario, Alice will be the channel initiator while bob
// will act as the responder to the workflow.
@ -811,6 +814,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
}
aliceReq := &lnwallet.InitFundingReserveMsg{
ChainHash: chainHash,
PendingChanID: pendingChanID,
NodeID: bobPub,
NodeAddr: bobAddr,
LocalFundingAmt: fundingAmt,
@ -820,6 +824,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
PushMSat: pushAmt,
Flags: lnwire.FFAnnounceChannel,
Tweakless: tweakless,
ChanFunder: aliceChanFunder,
}
aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
if err != nil {
@ -839,15 +844,20 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("unable to verify constraints: %v", err)
}
// Verify all contribution fields have been set properly.
// Verify all contribution fields have been set properly, but only if
// Alice is the funder herself.
aliceContribution := aliceChanReservation.OurContribution()
if fetchFundingTx == nil {
if len(aliceContribution.Inputs) < 1 {
t.Fatalf("outputs for funding tx not properly selected, have %v "+
"outputs should at least 1", len(aliceContribution.Inputs))
t.Fatalf("outputs for funding tx not properly "+
"selected, have %v outputs should at least 1",
len(aliceContribution.Inputs))
}
if len(aliceContribution.ChangeOutputs) != 1 {
t.Fatalf("coin selection failed, should have one change outputs, "+
"instead have: %v", len(aliceContribution.ChangeOutputs))
t.Fatalf("coin selection failed, should have one "+
"change outputs, instead have: %v",
len(aliceContribution.ChangeOutputs))
}
}
assertContributionInitPopulated(t, aliceContribution)
@ -855,6 +865,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// reservation initiation, then consume Alice's contribution.
bobReq := &lnwallet.InitFundingReserveMsg{
ChainHash: chainHash,
PendingChanID: pendingChanID,
NodeID: alicePub,
NodeAddr: aliceAddr,
LocalFundingAmt: 0,
@ -897,10 +908,11 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// At this point, Alice should have generated all the signatures
// required for the funding transaction, as well as Alice's commitment
// signature to bob.
// signature to bob, but only if the funding transaction was
// constructed internally.
aliceRemoteContribution := aliceChanReservation.TheirContribution()
aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures()
if aliceFundingSigs == nil {
if fetchFundingTx == nil && aliceFundingSigs == nil {
t.Fatalf("funding sigs not found")
}
if aliceCommitSig == nil {
@ -909,7 +921,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
// Additionally, the funding tx and the funding outpoint should have
// been populated.
if aliceChanReservation.FinalFundingTx() == nil {
if aliceChanReservation.FinalFundingTx() == nil && fetchFundingTx == nil {
t.Fatalf("funding transaction never created!")
}
if aliceChanReservation.FundingOutpoint() == nil {
@ -951,9 +963,17 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("alice unable to complete reservation: %v", err)
}
// If the caller provided an alternative way to obtain the funding tx,
// then we'll use that. Otherwise, we'll obtain it directly from Alice.
var fundingTx *wire.MsgTx
if fetchFundingTx != nil {
fundingTx = fetchFundingTx()
} else {
fundingTx = aliceChanReservation.FinalFundingTx()
}
// The resulting active channel state should have been persisted to the
// DB for both Alice and Bob.
fundingTx := aliceChanReservation.FinalFundingTx()
fundingSha := fundingTx.TxHash()
aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub)
if err != nil {
@ -2523,7 +2543,8 @@ var walletTests = []walletTestCase{
bob *lnwallet.LightningWallet, t *testing.T) {
testSingleFunderReservationWorkflow(
miner, alice, bob, t, false,
miner, alice, bob, t, false, nil, nil,
[32]byte{},
)
},
},
@ -2533,10 +2554,15 @@ var walletTests = []walletTestCase{
bob *lnwallet.LightningWallet, t *testing.T) {
testSingleFunderReservationWorkflow(
miner, alice, bob, t, true,
miner, alice, bob, t, true, nil, nil,
[32]byte{},
)
},
},
{
name: "single funding workflow external funding tx",
test: testSingleFunderExternalFundingTx,
},
{
name: "dual funder workflow",
test: testDualFundingReservationWorkflow,
@ -2675,6 +2701,114 @@ func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error {
return nil
}
// testSingleFunderExternalFundingTx tests that the wallet is able to properly
// carry out a funding flow backed by a channel point that has been crafted
// outside the wallet.
func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) {
// First, we'll obtain multi-sig keys from both Alice and Bob which
// simulates them exchanging keys on a higher level.
aliceFundingKey, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig)
if err != nil {
t.Fatalf("unable to obtain alice funding key: %v", err)
}
bobFundingKey, err := bob.DeriveNextKey(keychain.KeyFamilyMultiSig)
if err != nil {
t.Fatalf("unable to obtain bob funding key: %v", err)
}
// We'll now set up for them to open a 4 BTC channel, with 1 BTC pushed
// to Bob's side.
chanAmt := 4 * btcutil.SatoshiPerBitcoin
// Simulating external funding negotiation, we'll now create the
// funding transaction for both parties. Utilizing existing tools,
// we'll create a new chanfunding.Assembler hacked by Alice's wallet.
aliceChanFunder := chanfunding.NewWalletAssembler(chanfunding.WalletConfig{
CoinSource: lnwallet.NewCoinSource(alice),
CoinSelectLocker: alice,
CoinLocker: alice,
Signer: alice.Cfg.Signer,
DustLimit: 600,
})
// With the chan funder created, we'll now provision a funding intent,
// bind the keys we obtained above, and finally obtain our funding
// transaction and outpoint.
fundingIntent, err := aliceChanFunder.ProvisionChannel(&chanfunding.Request{
LocalAmt: btcutil.Amount(chanAmt),
MinConfs: 1,
FeeRate: 253,
ChangeAddr: func() (btcutil.Address, error) {
return alice.NewAddress(lnwallet.WitnessPubKey, true)
},
})
if err != nil {
t.Fatalf("unable to perform coin selection: %v", err)
}
// With our intent created, we'll instruct it to finalize the funding
// transaction, and also hand us the outpoint so we can simulate
// external crafting of the funding transaction.
var (
fundingTx *wire.MsgTx
chanPoint *wire.OutPoint
)
if fullIntent, ok := fundingIntent.(*chanfunding.FullIntent); ok {
fullIntent.BindKeys(&aliceFundingKey, bobFundingKey.PubKey)
fundingTx, err = fullIntent.CompileFundingTx(nil, nil)
if err != nil {
t.Fatalf("unable to compile funding tx: %v", err)
}
chanPoint, err = fullIntent.ChanPoint()
if err != nil {
t.Fatalf("unable to obtain chan point: %v", err)
}
} else {
t.Fatalf("expected full intent, instead got: %T", fullIntent)
}
// Now that we have the fully constructed funding transaction, we'll
// create a new shim external funder out of it for Alice, and prep a
// shim intent for Bob.
aliceExternalFunder := chanfunding.NewCannedAssembler(
*chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey,
bobFundingKey.PubKey, true,
)
bobShimIntent, err := chanfunding.NewCannedAssembler(
*chanPoint, btcutil.Amount(chanAmt), &bobFundingKey,
aliceFundingKey.PubKey, false,
).ProvisionChannel(nil)
if err != nil {
t.Fatalf("unable to create shim intent for bob: %v", err)
}
// At this point, we have everything we need to carry out our test, so
// we'll being the funding flow between Alice and Bob.
//
// However, before we do so, we'll register a new shim intent for Bob,
// so he knows what keys to use when he receives the funding request
// from Alice.
pendingChanID := testHdSeed
err = bob.RegisterFundingIntent(pendingChanID, bobShimIntent)
if err != nil {
t.Fatalf("unable to register intent: %v", err)
}
// Now we can carry out the single funding flow as normal, we'll
// specify our external funder and funding transaction, as well as the
// pending channel ID generated above to allow Alice and Bob to track
// the funding flow externally.
testSingleFunderReservationWorkflow(
miner, alice, bob, t, true, aliceExternalFunder,
func() *wire.MsgTx {
return fundingTx
}, pendingChanID,
)
}
// TestInterfaces tests all registered interfaces with a unified set of tests
// which exercise each of the required methods found within the WalletController
// interface.
@ -2737,8 +2871,10 @@ func TestLightningWallet(t *testing.T) {
for _, walletDriver := range lnwallet.RegisteredWallets() {
for _, backEnd := range walletDriver.BackEnds() {
runTests(t, walletDriver, backEnd, miningNode,
rpcConfig, chainNotifier)
if !runTests(t, walletDriver, backEnd, miningNode,
rpcConfig, chainNotifier) {
return
}
}
}
}
@ -2750,7 +2886,7 @@ func TestLightningWallet(t *testing.T) {
func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
backEnd string, miningNode *rpctest.Harness,
rpcConfig rpcclient.ConnConfig,
chainNotifier *btcdnotify.BtcdNotifier) {
chainNotifier chainntnfs.ChainNotifier) bool {
var (
bio lnwallet.BlockChainIO
@ -2979,8 +3115,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
bob, err := createTestWallet(
tempTestDirBob, miningNode, netParams,
chainNotifier, bobWalletController, bobKeyRing,
bobSigner, bio,
chainNotifier, bobWalletController, bobKeyRing, bobSigner, bio,
)
if err != nil {
t.Fatalf("unable to create test ln wallet: %v", err)
@ -2995,13 +3130,22 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
// Execute every test, clearing possibly mutated
// wallet state after each step.
for _, walletTest := range walletTests {
walletTest := walletTest
testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd,
walletTest.name)
success := t.Run(testName, func(t *testing.T) {
if backEnd == "neutrino" &&
strings.Contains(walletTest.name, "dual funder") {
t.Skip("skipping dual funder tests for neutrino")
}
return
walletTest.test(miningNode, alice, bob, t)
})
if !success {
break
return false
}
// TODO(roasbeef): possible reset mining
@ -3012,4 +3156,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
t.Fatalf("unable to wipe wallet state: %v", err)
}
}
return true
}

@ -11,6 +11,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -109,19 +110,19 @@ type ChannelReservation struct {
// throughout its lifetime.
reservationID uint64
// pendingChanID is the pending channel ID for this channel as
// identified within the wire protocol.
pendingChanID [32]byte
// pushMSat the amount of milli-satoshis that should be pushed to the
// responder of a single funding channel as part of the initial
// commitment state.
pushMSat lnwire.MilliSatoshi
// chanOpen houses a struct containing the channel and additional
// confirmation details will be sent on once the channel is considered
// 'open'. A channel is open once the funding transaction has reached a
// sufficient number of confirmations.
chanOpen chan *openChanDetails
chanOpenErr chan error
wallet *LightningWallet
chanFunder chanfunding.Assembler
fundingIntent chanfunding.Intent
}
// NewChannelReservation creates a new channel reservation. This function is
@ -131,8 +132,9 @@ type ChannelReservation struct {
func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet,
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
flags lnwire.FundingFlag,
tweaklessCommit bool) (*ChannelReservation, error) {
flags lnwire.FundingFlag, tweaklessCommit bool,
fundingAssembler chanfunding.Assembler,
pendingChanID [32]byte) (*ChannelReservation, error) {
var (
ourBalance lnwire.MilliSatoshi
@ -220,6 +222,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
} else {
chanType |= channeldb.SingleFunderBit
}
// If this intent isn't one that's able to provide us with a
// funding transaction, then we'll set the chanType bit to
// signal that we don't have access to one.
if _, ok := fundingAssembler.(chanfunding.FundingTxAssembler); !ok {
chanType |= channeldb.NoFundingTxBit
}
} else {
// Otherwise, this is a dual funder channel, and no side is
// technically the "initiator"
@ -258,10 +268,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
Db: wallet.Cfg.Database,
},
pushMSat: pushMSat,
pendingChanID: pendingChanID,
reservationID: id,
chanOpen: make(chan *openChanDetails, 1),
chanOpenErr: make(chan error, 1),
wallet: wallet,
chanFunder: fundingAssembler,
}, nil
}

@ -5,7 +5,6 @@ import (
"crypto/sha256"
"errors"
"fmt"
"math"
"net"
"sync"
"sync/atomic"
@ -22,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
@ -33,20 +33,6 @@ const (
msgBufferSize = 100
)
// ErrInsufficientFunds is a type matching the error interface which is
// returned when coin selection for a new funding transaction fails to due
// having an insufficient amount of confirmed funds.
type ErrInsufficientFunds struct {
amountAvailable btcutil.Amount
amountSelected btcutil.Amount
}
func (e *ErrInsufficientFunds) Error() string {
return fmt.Sprintf("not enough witness outputs to create funding transaction,"+
" need %v only have %v available", e.amountAvailable,
e.amountSelected)
}
// InitFundingReserveMsg is the first message sent to initiate the workflow
// required to open a payment channel with a remote peer. The initial required
// parameters are configurable across channels. These parameters are to be
@ -62,6 +48,10 @@ type InitFundingReserveMsg struct {
// target channel.
ChainHash *chainhash.Hash
// PendingChanID is the pending channel ID for this funding flow as
// used in the wire protocol.
PendingChanID [32]byte
// NodeID is the ID of the remote node we would like to open a channel
// with.
NodeID *btcec.PublicKey
@ -112,6 +102,12 @@ type InitFundingReserveMsg struct {
// commitment format or not.
Tweakless bool
// ChanFunder is an optional channel funder that allows the caller to
// control exactly how the channel funding is carried out. If not
// specified, then the default chanfunding.WalletAssembler will be
// used.
ChanFunder chanfunding.Assembler
// err is a channel in which all errors will be sent across. Will be
// nil if this initial set is successful.
//
@ -281,6 +277,11 @@ type LightningWallet struct {
// the currently locked outpoints.
lockedOutPoints map[wire.OutPoint]struct{}
// fundingIntents houses all the "interception" registered by a caller
// using the RegisterFundingIntent method.
intentMtx sync.RWMutex
fundingIntents map[[32]byte]chanfunding.Intent
quit chan struct{}
wg sync.WaitGroup
@ -301,6 +302,7 @@ func NewLightningWallet(Cfg Config) (*LightningWallet, error) {
nextFundingID: 0,
fundingLimbo: make(map[uint64]*ChannelReservation),
lockedOutPoints: make(map[wire.OutPoint]struct{}),
fundingIntents: make(map[[32]byte]chanfunding.Intent),
quit: make(chan struct{}),
}, nil
}
@ -439,6 +441,21 @@ func (l *LightningWallet) InitChannelReservation(
return <-req.resp, <-req.err
}
// RegisterFundingIntent allows a caller to signal to the wallet that if a
// pending channel ID of expectedID is found, then it can skip constructing a
// new chanfunding.Assembler, and instead use the specified chanfunding.Intent.
// As an example, this lets some of the parameters for funding transaction to
// be negotiated outside the regular funding protocol.
func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
shimIntent chanfunding.Intent) error {
l.intentMtx.Lock()
l.fundingIntents[expectedID] = shimIntent
l.intentMtx.Unlock()
return nil
}
// handleFundingReserveRequest processes a message intending to create, and
// validate a funding reservation request.
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
@ -461,24 +478,55 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return
}
// If no chanFunder was provided, then we'll assume the default
// assembler, which is backed by the wallet's internal coin selection.
if req.ChanFunder == nil {
cfg := chanfunding.WalletConfig{
CoinSource: &CoinSource{l},
CoinSelectLocker: l,
CoinLocker: l,
Signer: l.Cfg.Signer,
DustLimit: DefaultDustLimit(),
}
req.ChanFunder = chanfunding.NewWalletAssembler(cfg)
}
localFundingAmt := req.LocalFundingAmt
remoteFundingAmt := req.RemoteFundingAmt
var (
selected *coinSelection
fundingIntent chanfunding.Intent
err error
)
// If we're on the receiving end of a single funder channel then we
// don't need to perform any coin selection, and the remote contributes
// all funds. Otherwise, attempt to obtain enough coins to meet the
// required funding amount.
if req.LocalFundingAmt != 0 {
// If we've just received an inbound funding request that we have a
// registered shim intent to, then we'll obtain the backing intent now.
// In this case, we're doing a special funding workflow that allows
// more advanced constructions such as channel factories to be
// instantiated.
l.intentMtx.Lock()
fundingIntent, ok := l.fundingIntents[req.PendingChanID]
l.intentMtx.Unlock()
// Otherwise, this is a normal funding flow, so we'll use the chan
// funder in the attached request to provision the inputs/outputs
// that'll ultimately be used to construct the funding transaction.
if !ok {
// Coin selection is done on the basis of sat/kw, so we'll use
// the fee rate passed in to perform coin selection.
var err error
selected, err = l.selectCoinsAndChange(
req.FundingFeePerKw, req.LocalFundingAmt, req.MinConfs,
req.SubtractFees,
fundingReq := &chanfunding.Request{
RemoteAmt: req.RemoteFundingAmt,
LocalAmt: req.LocalFundingAmt,
MinConfs: req.MinConfs,
SubtractFees: req.SubtractFees,
FeeRate: req.FundingFeePerKw,
ChangeAddr: func() (btcutil.Address, error) {
return l.NewAddress(WitnessPubKey, true)
},
}
fundingIntent, err = req.ChanFunder.ProvisionChannel(
fundingReq,
)
if err != nil {
req.err <- err
@ -486,31 +534,51 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return
}
localFundingAmt = selected.fundingAmt
localFundingAmt = fundingIntent.LocalFundingAmt()
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
}
// The total channel capacity will be the size of the funding output we
// created plus the remote contribution.
capacity := localFundingAmt + req.RemoteFundingAmt
capacity := localFundingAmt + remoteFundingAmt
id := atomic.AddUint64(&l.nextFundingID, 1)
reservation, err := NewChannelReservation(
capacity, localFundingAmt, req.CommitFeePerKw, l, id,
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
req.Tweakless,
req.Tweakless, req.ChanFunder, req.PendingChanID,
)
if err != nil {
selected.unlockCoins()
if fundingIntent != nil {
fundingIntent.Cancel()
}
req.err <- err
req.resp <- nil
return
}
var keyRing keychain.KeyRing = l.SecretKeyRing
// If this is a shim intent, then it may be attempting to use an
// existing set of keys for the funding workflow. In this case, we'll
// make a simple wrapper keychain.KeyRing that will proxy certain
// derivation calls to future callers.
if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok {
keyRing = &shimKeyRing{
KeyRing: keyRing,
ShimIntent: shimIntent,
}
}
err = l.initOurContribution(
reservation, selected, req.NodeAddr, req.NodeID,
reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
)
if err != nil {
selected.unlockCoins()
if fundingIntent != nil {
fundingIntent.Cancel()
}
req.err <- err
req.resp <- nil
return
@ -533,52 +601,60 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
// and change reserved for the channel, and derives the keys to use for this
// channel.
func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
selected *coinSelection, nodeAddr net.Addr, nodeID *btcec.PublicKey) error {
fundingIntent chanfunding.Intent, nodeAddr net.Addr,
nodeID *btcec.PublicKey, keyRing keychain.KeyRing) error {
// Grab the mutex on the ChannelReservation to ensure thread-safety
reservation.Lock()
defer reservation.Unlock()
if selected != nil {
reservation.ourContribution.Inputs = selected.coins
reservation.ourContribution.ChangeOutputs = selected.change
// At this point, if we have a funding intent, we'll use it to populate
// the existing reservation state entries for our coin selection.
if fundingIntent != nil {
if intent, ok := fundingIntent.(*chanfunding.FullIntent); ok {
for _, coin := range intent.InputCoins {
reservation.ourContribution.Inputs = append(
reservation.ourContribution.Inputs,
&wire.TxIn{
PreviousOutPoint: coin.OutPoint,
},
)
}
reservation.ourContribution.ChangeOutputs = intent.ChangeOutputs
}
reservation.fundingIntent = fundingIntent
}
reservation.nodeAddr = nodeAddr
reservation.partialState.IdentityPub = nodeID
// Next, we'll grab a series of keys from the wallet which will be used
// for the duration of the channel. The keys include: our multi-sig
// key, the base revocation key, the base htlc key,the base payment
// key, and the delayed payment key.
//
// TODO(roasbeef): "salt" each key as well?
var err error
reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey(
reservation.ourContribution.MultiSigKey, err = keyRing.DeriveNextKey(
keychain.KeyFamilyMultiSig,
)
if err != nil {
return err
}
reservation.ourContribution.RevocationBasePoint, err = l.DeriveNextKey(
reservation.ourContribution.RevocationBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationBase,
)
if err != nil {
return err
}
reservation.ourContribution.HtlcBasePoint, err = l.DeriveNextKey(
reservation.ourContribution.HtlcBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyHtlcBase,
)
if err != nil {
return err
}
reservation.ourContribution.PaymentBasePoint, err = l.DeriveNextKey(
reservation.ourContribution.PaymentBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyPaymentBase,
)
if err != nil {
return err
}
reservation.ourContribution.DelayBasePoint, err = l.DeriveNextKey(
reservation.ourContribution.DelayBasePoint, err = keyRing.DeriveNextKey(
keychain.KeyFamilyDelayBase,
)
if err != nil {
@ -587,7 +663,7 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
// With the above keys created, we'll also need to initialization our
// initial revocation tree state.
nextRevocationKeyDesc, err := l.DeriveNextKey(
nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
keychain.KeyFamilyRevocationRoot,
)
if err != nil {
@ -653,6 +729,16 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
delete(l.fundingLimbo, req.pendingFundingID)
pid := pendingReservation.pendingChanID
l.intentMtx.Lock()
if intent, ok := l.fundingIntents[pid]; ok {
intent.Cancel()
delete(l.fundingIntents, pendingReservation.pendingChanID)
}
l.intentMtx.Unlock()
req.err <- nil
}
@ -722,100 +808,82 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
pendingReservation.Lock()
defer pendingReservation.Unlock()
// Create a blank, fresh transaction. Soon to be a complete funding
// transaction which will allow opening a lightning channel.
pendingReservation.fundingTx = wire.NewMsgTx(1)
fundingTx := pendingReservation.fundingTx
// Some temporary variables to cut down on the resolution verbosity.
pendingReservation.theirContribution = req.contribution
theirContribution := req.contribution
ourContribution := pendingReservation.ourContribution
// Add all multi-party inputs and outputs to the transaction.
for _, ourInput := range ourContribution.Inputs {
fundingTx.AddTxIn(ourInput)
}
for _, theirInput := range theirContribution.Inputs {
fundingTx.AddTxIn(theirInput)
}
for _, ourChangeOutput := range ourContribution.ChangeOutputs {
fundingTx.AddTxOut(ourChangeOutput)
}
for _, theirChangeOutput := range theirContribution.ChangeOutputs {
fundingTx.AddTxOut(theirChangeOutput)
}
ourKey := pendingReservation.ourContribution.MultiSigKey
theirKey := theirContribution.MultiSigKey
// Finally, add the 2-of-2 multi-sig output which will set up the lightning
// channel.
channelCapacity := int64(pendingReservation.partialState.Capacity)
witnessScript, multiSigOut, err := input.GenFundingPkScript(
ourKey.PubKey.SerializeCompressed(),
theirKey.PubKey.SerializeCompressed(), channelCapacity,
var (
chanPoint *wire.OutPoint
err error
)
// At this point, we can now construct our channel point. Depending on
// which type of intent we obtained from our chanfunding.Assembler,
// we'll carry out a distinct set of steps.
switch fundingIntent := pendingReservation.fundingIntent.(type) {
case *chanfunding.ShimIntent:
chanPoint, err = fundingIntent.ChanPoint()
if err != nil {
req.err <- err
req.err <- fmt.Errorf("unable to obtain chan point: %v", err)
return
}
// Sort the transaction. Since both side agree to a canonical ordering,
// by sorting we no longer need to send the entire transaction. Only
// signatures will be exchanged.
fundingTx.AddTxOut(multiSigOut)
txsort.InPlaceSort(pendingReservation.fundingTx)
pendingReservation.partialState.FundingOutpoint = *chanPoint
// Next, sign all inputs that are ours, collecting the signatures in
// order of the inputs.
pendingReservation.ourFundingInputScripts = make([]*input.Script, 0,
len(ourContribution.Inputs))
signDesc := input.SignDescriptor{
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(fundingTx),
case *chanfunding.FullIntent:
// Now that we know their public key, we can bind theirs as
// well as ours to the funding intent.
fundingIntent.BindKeys(
&pendingReservation.ourContribution.MultiSigKey,
theirContribution.MultiSigKey.PubKey,
)
// With our keys bound, we can now construct+sign the final
// funding transaction and also obtain the chanPoint that
// creates the channel.
fundingTx, err := fundingIntent.CompileFundingTx(
theirContribution.Inputs,
theirContribution.ChangeOutputs,
)
if err != nil {
req.err <- fmt.Errorf("unable to construct funding "+
"tx: %v", err)
return
}
for i, txIn := range fundingTx.TxIn {
info, err := l.FetchInputInfo(&txIn.PreviousOutPoint)
if err == ErrNotMine {
chanPoint, err = fundingIntent.ChanPoint()
if err != nil {
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
} 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,
inputScript,
&input.Script{
Witness: txIn.Witness,
SigScript: txIn.SignatureScript,
},
)
}
// Locate the index of the multi-sig outpoint in order to record it
// since the outputs are canonically sorted. If this is a single funder
// workflow, then we'll also need to send this to the remote node.
fundingTxID := fundingTx.TxHash()
_, multiSigIndex := input.FindScriptOutputIndex(fundingTx, multiSigOut.PkScript)
fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex)
pendingReservation.partialState.FundingOutpoint = *fundingOutpoint
walletLog.Debugf("Funding tx for ChannelPoint(%v) generated: %v",
fundingOutpoint, spew.Sdump(fundingTx))
walletLog.Debugf("Funding tx for ChannelPoint(%v) "+
"generated: %v", chanPoint, spew.Sdump(fundingTx))
}
// Initialize an empty sha-chain for them, tracking the current pending
// revocation hash (we don't yet know the preimage so we can't add it
@ -832,10 +900,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
// Create the txin to our commitment transaction; required to construct
// the commitment transactions.
fundingTxIn := wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: fundingTxID,
Index: multiSigIndex,
},
PreviousOutPoint: *chanPoint,
}
// With the funding tx complete, create both commitment transactions.
@ -892,21 +957,32 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
txsort.InPlaceSort(theirCommitTx)
walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v",
fundingOutpoint, spew.Sdump(ourCommitTx))
chanPoint, spew.Sdump(ourCommitTx))
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v",
fundingOutpoint, spew.Sdump(theirCommitTx))
chanPoint, spew.Sdump(theirCommitTx))
// Record newly available information within the open channel state.
chanState.FundingOutpoint = *fundingOutpoint
chanState.FundingOutpoint = *chanPoint
chanState.LocalCommitment.CommitTx = ourCommitTx
chanState.RemoteCommitment.CommitTx = theirCommitTx
// Next, we'll obtain the funding witness script, and the funding
// output itself so we can generate a valid signature for the remote
// party.
fundingIntent := pendingReservation.fundingIntent
fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput()
if err != nil {
req.err <- fmt.Errorf("unable to obtain funding output")
return
}
// Generate a signature for their version of the initial commitment
// transaction.
signDesc = input.SignDescriptor{
WitnessScript: witnessScript,
ourKey := ourContribution.MultiSigKey
signDesc := input.SignDescriptor{
WitnessScript: fundingWitnessScript,
KeyDesc: ourKey,
Output: multiSigOut,
Output: fundingOutput,
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(theirCommitTx),
InputIndex: 0,
@ -962,12 +1038,64 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg
return
}
// openChanDetails contains a "finalized" channel which can be considered
// "open" according to the requested confirmation depth at reservation
// initialization. Additionally, the struct contains additional details
// pertaining to the exact location in the main chain in-which the transaction
// was confirmed.
type openChanDetails struct {
// verifyFundingInputs attempts to verify all remote inputs to the funding
// transaction.
func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx,
remoteInputScripts []*input.Script) error {
sigIndex := 0
fundingHashCache := txscript.NewTxSigHashes(fundingTx)
inputScripts := remoteInputScripts
for i, txin := range fundingTx.TxIn {
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
// Attach the input scripts so we can verify it below.
txin.Witness = inputScripts[sigIndex].Witness
txin.SignatureScript = inputScripts[sigIndex].SigScript
// Fetch the alleged previous output along with the
// pkscript referenced by this input.
//
// TODO(roasbeef): when dual funder pass actual
// height-hint
//
// TODO(roasbeef): this fails for neutrino always as it
// treats the height hint as an exact birthday of the
// utxo rather than a lower bound
pkScript, err := txscript.ComputePkScript(
txin.SignatureScript, txin.Witness,
)
if err != nil {
return fmt.Errorf("cannot create script: %v", err)
}
output, err := l.Cfg.ChainIO.GetUtxo(
&txin.PreviousOutPoint,
pkScript.Script(), 0, l.quit,
)
if output == nil {
return fmt.Errorf("input to funding tx does "+
"not exist: %v", err)
}
// Ensure that the witness+sigScript combo is valid.
vm, err := txscript.NewEngine(
output.PkScript, fundingTx, i,
txscript.StandardVerifyFlags, nil,
fundingHashCache, output.Value,
)
if err != nil {
return fmt.Errorf("cannot create script "+
"engine: %s", err)
}
if err = vm.Execute(); err != nil {
return fmt.Errorf("cannot validate "+
"transaction: %s", err)
}
sigIndex++
}
}
return nil
}
// handleFundingCounterPartySigs is the final step in the channel reservation
@ -994,60 +1122,18 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
// signatures to their inputs.
res.theirFundingInputScripts = msg.theirFundingInputScripts
inputScripts := msg.theirFundingInputScripts
// Only if we have the final funding transaction do we need to verify
// the final set of inputs. Otherwise, it may be the case that the
// channel was funded via an external wallet.
fundingTx := res.fundingTx
sigIndex := 0
fundingHashCache := txscript.NewTxSigHashes(fundingTx)
for i, txin := range fundingTx.TxIn {
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
// Attach the input scripts so we can verify it below.
txin.Witness = inputScripts[sigIndex].Witness
txin.SignatureScript = inputScripts[sigIndex].SigScript
// Fetch the alleged previous output along with the
// pkscript referenced by this input.
//
// TODO(roasbeef): when dual funder pass actual
// height-hint
pkScript, err := input.WitnessScriptHash(
txin.Witness[len(txin.Witness)-1],
)
if res.partialState.ChanType.HasFundingTx() {
err := l.verifyFundingInputs(fundingTx, inputScripts)
if err != nil {
msg.err <- fmt.Errorf("cannot create script: "+
"%v", err)
msg.err <- err
msg.completeChan <- nil
return
}
output, err := l.Cfg.ChainIO.GetUtxo(
&txin.PreviousOutPoint,
pkScript, 0, l.quit,
)
if output == nil {
msg.err <- fmt.Errorf("input to funding tx "+
"does not exist: %v", err)
msg.completeChan <- nil
return
}
// Ensure that the witness+sigScript combo is valid.
vm, err := txscript.NewEngine(output.PkScript,
fundingTx, i, txscript.StandardVerifyFlags, nil,
fundingHashCache, output.Value)
if err != nil {
msg.err <- fmt.Errorf("cannot create script "+
"engine: %s", err)
msg.completeChan <- nil
return
}
if err = vm.Execute(); err != nil {
msg.err <- fmt.Errorf("cannot validate "+
"transaction: %s", err)
msg.completeChan <- nil
return
}
sigIndex++
}
}
// 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.
channelValue := int64(res.partialState.Capacity)
hashCache := txscript.NewTxSigHashes(commitTx)
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache,
txscript.SigHashAll, commitTx, 0, channelValue)
sigHash, err := txscript.CalcWitnessSigHash(
witnessScript, hashCache, txscript.SigHashAll, commitTx,
0, channelValue,
)
if err != nil {
msg.err <- err
msg.completeChan <- nil
@ -1103,6 +1191,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
delete(l.fundingLimbo, res.reservationID)
l.limboMtx.Unlock()
l.intentMtx.Lock()
delete(l.fundingIntents, res.pendingChanID)
l.intentMtx.Unlock()
// As we're about to broadcast the funding transaction, we'll take note
// of the current height for record keeping purposes.
//
@ -1223,8 +1315,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
return
}
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache,
txscript.SigHashAll, ourCommitTx, 0, channelValue)
sigHash, err := txscript.CalcWitnessSigHash(
witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0,
channelValue,
)
if err != nil {
req.err <- err
req.completeChan <- nil
@ -1238,7 +1332,8 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
req.err <- err
req.completeChan <- nil
return
} else if !sig.Verify(sigHash, theirKey.PubKey) {
}
if !sig.Verify(sigHash, theirKey.PubKey) {
req.err <- fmt.Errorf("counterparty's commitment signature " +
"is invalid")
req.completeChan <- nil
@ -1298,6 +1393,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
l.limboMtx.Lock()
delete(l.fundingLimbo, req.pendingFundingID)
l.limboMtx.Unlock()
l.intentMtx.Lock()
delete(l.fundingIntents, pendingReservation.pendingChanID)
l.intentMtx.Unlock()
}
// WithCoinSelectLock will execute the passed function closure in a
@ -1311,127 +1410,6 @@ func (l *LightningWallet) WithCoinSelectLock(f func() error) error {
return f()
}
// coinSelection holds the result from selectCoinsAndChange.
type coinSelection struct {
coins []*wire.TxIn
change []*wire.TxOut
fundingAmt btcutil.Amount
unlockCoins func()
}
// selectCoinsAndChange performs coin selection in order to obtain witness
// outputs which sum to at least 'amt' amount of satoshis. If necessary,
// a change address will also be generated. If coin selection is
// successful/possible, then the selected coins and change outputs are
// returned, and the value of the resulting funding output. This method locks
// the selected outputs, and a function closure to unlock them in case of an
// error is returned.
func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight,
amt btcutil.Amount, minConfs int32, subtractFees bool) (
*coinSelection, error) {
// We hold the coin select mutex while querying for outputs, and
// performing coin selection in order to avoid inadvertent double
// spends across funding transactions.
l.coinSelectMtx.Lock()
defer l.coinSelectMtx.Unlock()
walletLog.Infof("Performing funding tx coin selection using %v "+
"sat/kw as fee rate", int64(feeRate))
// Find all unlocked unspent witness outputs that satisfy the minimum
// number of confirmations required.
coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32)
if err != nil {
return nil, err
}
var (
selectedCoins []*Utxo
fundingAmt btcutil.Amount
changeAmt btcutil.Amount
)
// Perform coin selection over our available, unlocked unspent outputs
// in order to find enough coins to meet the funding amount
// requirements.
switch {
// In case this request want the fees subtracted from the local amount,
// we'll call the specialized method for that. This ensures that we
// won't deduct more that the specified balance from our wallet.
case subtractFees:
dustLimit := l.Cfg.DefaultConstraints.DustLimit
selectedCoins, fundingAmt, changeAmt, err = coinSelectSubtractFees(
feeRate, amt, dustLimit, coins,
)
if err != nil {
return nil, err
}
// Ótherwise do a normal coin selection where we target a given funding
// amount.
default:
fundingAmt = amt
selectedCoins, changeAmt, err = coinSelect(feeRate, amt, coins)
if err != nil {
return nil, err
}
}
// Record any change output(s) generated as a result of the coin
// selection, but only if the addition of the output won't lead to the
// creation of dust.
var changeOutputs []*wire.TxOut
if changeAmt != 0 && changeAmt > DefaultDustLimit() {
changeAddr, err := l.NewAddress(WitnessPubKey, true)
if err != nil {
return nil, err
}
changeScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, err
}
changeOutputs = make([]*wire.TxOut, 1)
changeOutputs[0] = &wire.TxOut{
Value: int64(changeAmt),
PkScript: changeScript,
}
}
// Lock the selected coins. These coins are now "reserved", this
// prevents concurrent funding requests from referring to and this
// double-spending the same set of coins.
inputs := make([]*wire.TxIn, len(selectedCoins))
for i, coin := range selectedCoins {
outpoint := &coin.OutPoint
l.lockedOutPoints[*outpoint] = struct{}{}
l.LockOutpoint(*outpoint)
// Empty sig script, we'll actually sign if this reservation is
// queued up to be completed (the other side accepts).
inputs[i] = wire.NewTxIn(outpoint, nil, nil)
}
unlock := func() {
l.coinSelectMtx.Lock()
defer l.coinSelectMtx.Unlock()
for _, coin := range selectedCoins {
outpoint := &coin.OutPoint
delete(l.lockedOutPoints, *outpoint)
l.UnlockOutpoint(*outpoint)
}
}
return &coinSelection{
coins: inputs,
change: changeOutputs,
fundingAmt: fundingAmt,
unlockCoins: unlock,
}, nil
}
// DeriveStateHintObfuscator derives the bytes to be used for obfuscating the
// state hints from the root to be used for a new channel. The obfuscator is
// generated via the following computation:
@ -1468,179 +1446,6 @@ func initStateHints(commit1, commit2 *wire.MsgTx,
return nil
}
// selectInputs selects a slice of inputs necessary to meet the specified
// selection amount. If input selection is unable to succeed due to insufficient
// funds, a non-nil error is returned. Additionally, the total amount of the
// selected coins are returned in order for the caller to properly handle
// change+fees.
func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*Utxo, error) {
satSelected := btcutil.Amount(0)
for i, coin := range coins {
satSelected += coin.Value
if satSelected >= amt {
return satSelected, coins[:i+1], nil
}
}
return 0, nil, &ErrInsufficientFunds{amt, satSelected}
}
// coinSelect attempts to select a sufficient amount of coins, including a
// change output to fund amt satoshis, adhering to the specified fee rate. The
// specified fee rate should be expressed in sat/kw for coin selection to
// function properly.
func coinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
coins []*Utxo) ([]*Utxo, btcutil.Amount, error) {
amtNeeded := amt
for {
// First perform an initial round of coin selection to estimate
// the required fee.
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
if err != nil {
return nil, 0, err
}
var weightEstimate input.TxWeightEstimator
for _, utxo := range selectedUtxos {
switch utxo.AddressType {
case WitnessPubKey:
weightEstimate.AddP2WKHInput()
case NestedWitnessPubKey:
weightEstimate.AddNestedP2WKHInput()
default:
return nil, 0, fmt.Errorf("unsupported address type: %v",
utxo.AddressType)
}
}
// Channel funding multisig output is P2WSH.
weightEstimate.AddP2WSHOutput()
// Assume that change output is a P2WKH output.
//
// TODO: Handle wallets that generate non-witness change
// addresses.
// TODO(halseth): make coinSelect not estimate change output
// for dust change.
weightEstimate.AddP2WKHOutput()
// The difference between the selected amount and the amount
// requested will be used to pay fees, and generate a change
// output with the remaining.
overShootAmt := totalSat - amt
// Based on the estimated size and fee rate, if the excess
// amount isn't enough to pay fees, then increase the requested
// coin amount by the estimate required fee, performing another
// round of coin selection.
totalWeight := int64(weightEstimate.Weight())
requiredFee := feeRate.FeeForWeight(totalWeight)
if overShootAmt < requiredFee {
amtNeeded = amt + requiredFee
continue
}
// If the fee is sufficient, then calculate the size of the
// change output.
changeAmt := overShootAmt - requiredFee
return selectedUtxos, changeAmt, nil
}
}
// coinSelectSubtractFees attempts to select coins such that we'll spend up to
// amt in total after fees, adhering to the specified fee rate. The selected
// coins, the final output and change values are returned.
func coinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
dustLimit btcutil.Amount, coins []*Utxo) ([]*Utxo, btcutil.Amount,
btcutil.Amount, error) {
// First perform an initial round of coin selection to estimate
// the required fee.
totalSat, selectedUtxos, err := selectInputs(amt, coins)
if err != nil {
return nil, 0, 0, err
}
var weightEstimate input.TxWeightEstimator
for _, utxo := range selectedUtxos {
switch utxo.AddressType {
case WitnessPubKey:
weightEstimate.AddP2WKHInput()
case NestedWitnessPubKey:
weightEstimate.AddNestedP2WKHInput()
default:
return nil, 0, 0, fmt.Errorf("unsupported "+
"address type: %v", utxo.AddressType)
}
}
// Channel funding multisig output is P2WSH.
weightEstimate.AddP2WSHOutput()
// At this point we've got two possibilities, either create a
// change output, or not. We'll first try without creating a
// change output.
//
// Estimate the fee required for a transaction without a change
// output.
totalWeight := int64(weightEstimate.Weight())
requiredFee := feeRate.FeeForWeight(totalWeight)
// For a transaction without a change output, we'll let everything go
// to our multi-sig output after subtracting fees.
outputAmt := totalSat - requiredFee
changeAmt := btcutil.Amount(0)
// If the the output is too small after subtracting the fee, the coin
// selection cannot be performed with an amount this small.
if outputAmt <= dustLimit {
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
"subtracting fees(%v) below dust limit(%v)", outputAmt,
requiredFee, dustLimit)
}
// We were able to create a transaction with no change from the
// selected inputs. We'll remember the resulting values for
// now, while we try to add a change output. Assume that change output
// is a P2WKH output.
weightEstimate.AddP2WKHOutput()
// Now that we have added the change output, redo the fee
// estimate.
totalWeight = int64(weightEstimate.Weight())
requiredFee = feeRate.FeeForWeight(totalWeight)
// For a transaction with a change output, everything we don't spend
// will go to change.
newChange := totalSat - amt
newOutput := amt - requiredFee
// If adding a change output leads to both outputs being above
// the dust limit, we'll add the change output. Otherwise we'll
// go with the no change tx we originally found.
if newChange > dustLimit && newOutput > dustLimit {
outputAmt = newOutput
changeAmt = newChange
}
// Sanity check the resulting output values to make sure we
// don't burn a great part to fees.
totalOut := outputAmt + changeAmt
fee := totalSat - totalOut
// Fail if more than 20% goes to fees.
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
// total funding size?
if fee > totalOut/5 {
return nil, 0, 0, fmt.Errorf("fee %v on total output"+
"value %v", fee, totalOut)
}
return selectedUtxos, outputAmt, changeAmt, nil
}
// ValidateChannel will attempt to fully validate a newly mined channel, given
// its funding transaction and existing channel state. If this method returns
// an error, then the mined channel is invalid, and shouldn't be used.
@ -1695,3 +1500,81 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
return nil
}
// CoinSource is a wrapper around the wallet that implements the
// chanfunding.CoinSource interface.
type CoinSource struct {
wallet *LightningWallet
}
// NewCoinSource creates a new instance of the CoinSource wrapper struct.
func NewCoinSource(w *LightningWallet) *CoinSource {
return &CoinSource{wallet: w}
}
// ListCoins returns all UTXOs from the source that have between
// minConfs and maxConfs number of confirmations.
func (c *CoinSource) ListCoins(minConfs int32,
maxConfs int32) ([]chanfunding.Coin, error) {
utxos, err := c.wallet.ListUnspentWitness(minConfs, maxConfs)
if err != nil {
return nil, err
}
var coins []chanfunding.Coin
for _, utxo := range utxos {
coins = append(coins, chanfunding.Coin{
TxOut: wire.TxOut{
Value: int64(utxo.Value),
PkScript: utxo.PkScript,
},
OutPoint: utxo.OutPoint,
})
}
return coins, nil
}
// CoinFromOutPoint attempts to locate details pertaining to a coin based on
// its outpoint. If the coin isn't under the control of the backing CoinSource,
// then an error should be returned.
func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, error) {
inputInfo, err := c.wallet.FetchInputInfo(&op)
if err != nil {
return nil, err
}
return &chanfunding.Coin{
TxOut: wire.TxOut{
Value: int64(inputInfo.Value),
PkScript: inputInfo.PkScript,
},
OutPoint: inputInfo.OutPoint,
}, nil
}
// shimKeyRing is a wrapper struct that's used to provide the proper multi-sig
// key for an initiated external funding flow.
type shimKeyRing struct {
keychain.KeyRing
*chanfunding.ShimIntent
}
// DeriveNextKey intercepts the normal DeriveNextKey call to a keychain.KeyRing
// instance, and supplies the multi-sig key specified by the ShimIntent. This
// allows us to transparently insert new keys into the existing funding flow,
// as these keys may not come from the wallet itself.
func (s *shimKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) {
if keyFam != keychain.KeyFamilyMultiSig {
return s.KeyRing.DeriveNextKey(keyFam)
}
fundingKeys, err := s.ShimIntent.MultiSigKeys()
if err != nil {
return keychain.KeyDescriptor{}, err
}
return *fundingKeys.LocalKey, nil
}

2
log.go

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

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