Merge pull request #3659 from Roasbeef/external-funding-chanfunding
chanfunding: create new package to abstract over funding workflows
This commit is contained in:
commit
f2f79d3990
@ -147,6 +147,11 @@ const (
|
|||||||
// type, but it omits the tweak for one's key in the commitment
|
// type, but it omits the tweak for one's key in the commitment
|
||||||
// transaction of the remote party.
|
// transaction of the remote party.
|
||||||
SingleFunderTweaklessBit ChannelType = 1 << 1
|
SingleFunderTweaklessBit ChannelType = 1 << 1
|
||||||
|
|
||||||
|
// NoFundingTxBit denotes if we have the funding transaction locally on
|
||||||
|
// disk. This bit may be on if the funding transaction was crafted by a
|
||||||
|
// wallet external to the primary daemon.
|
||||||
|
NoFundingTxBit ChannelType = 1 << 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsSingleFunder returns true if the channel type if one of the known single
|
// IsSingleFunder returns true if the channel type if one of the known single
|
||||||
@ -166,6 +171,12 @@ func (c ChannelType) IsTweakless() bool {
|
|||||||
return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit
|
return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasFundingTx returns true if this channel type is one that has a funding
|
||||||
|
// transaction stored locally.
|
||||||
|
func (c ChannelType) HasFundingTx() bool {
|
||||||
|
return c&NoFundingTxBit == 0
|
||||||
|
}
|
||||||
|
|
||||||
// ChannelConstraints represents a set of constraints meant to allow a node to
|
// ChannelConstraints represents a set of constraints meant to allow a node to
|
||||||
// limit their exposure, enact flow control and ensure that all HTLCs are
|
// limit their exposure, enact flow control and ensure that all HTLCs are
|
||||||
// economically relevant. This struct will be mirrored for both sides of the
|
// economically relevant. This struct will be mirrored for both sides of the
|
||||||
@ -535,7 +546,9 @@ type OpenChannel struct {
|
|||||||
// is found to be pending.
|
// is found to be pending.
|
||||||
//
|
//
|
||||||
// NOTE: This value will only be populated for single-funder channels
|
// NOTE: This value will only be populated for single-funder channels
|
||||||
// for which we are the initiator.
|
// for which we are the initiator, and that we also have the funding
|
||||||
|
// transaction for. One can check this by using the HasFundingTx()
|
||||||
|
// method on the ChanType field.
|
||||||
FundingTxn *wire.MsgTx
|
FundingTxn *wire.MsgTx
|
||||||
|
|
||||||
// TODO(roasbeef): eww
|
// TODO(roasbeef): eww
|
||||||
@ -2522,6 +2535,16 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fundingTxPresent returns true if expect the funding transcation to be found
|
||||||
|
// on disk or already populated within the passed oen chanel struct.
|
||||||
|
func fundingTxPresent(channel *OpenChannel) bool {
|
||||||
|
chanType := channel.ChanType
|
||||||
|
|
||||||
|
return chanType.IsSingleFunder() && chanType.HasFundingTx() &&
|
||||||
|
channel.IsInitiator &&
|
||||||
|
!channel.hasChanStatus(ChanStatusRestored)
|
||||||
|
}
|
||||||
|
|
||||||
func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
|
func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
|
||||||
var w bytes.Buffer
|
var w bytes.Buffer
|
||||||
if err := WriteElements(&w,
|
if err := WriteElements(&w,
|
||||||
@ -2535,10 +2558,9 @@ func putChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single funder channels that we initiated, write the funding txn.
|
// For single funder channels that we initiated, and we have the
|
||||||
if channel.ChanType.IsSingleFunder() && channel.IsInitiator &&
|
// funding transaction, then write the funding txn.
|
||||||
!channel.hasChanStatus(ChanStatusRestored) {
|
if fundingTxPresent(channel) {
|
||||||
|
|
||||||
if err := WriteElement(&w, channel.FundingTxn); err != nil {
|
if err := WriteElement(&w, channel.FundingTxn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -2657,10 +2679,9 @@ func fetchChanInfo(chanBucket *bbolt.Bucket, channel *OpenChannel) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single funder channels that we initiated, read the funding txn.
|
// For single funder channels that we initiated and have the funding
|
||||||
if channel.ChanType.IsSingleFunder() && channel.IsInitiator &&
|
// transaction to, read the funding txn.
|
||||||
!channel.hasChanStatus(ChanStatusRestored) {
|
if fundingTxPresent(channel) {
|
||||||
|
|
||||||
if err := ReadElement(r, &channel.FundingTxn); err != nil {
|
if err := ReadElement(r, &channel.FundingTxn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -521,8 +521,9 @@ func (f *fundingManager) start() error {
|
|||||||
|
|
||||||
// Rebroadcast the funding transaction for any pending
|
// Rebroadcast the funding transaction for any pending
|
||||||
// channel that we initiated. No error will be returned
|
// channel that we initiated. No error will be returned
|
||||||
// if the transaction already has been broadcasted.
|
// if the transaction already has been broadcast.
|
||||||
if channel.ChanType.IsSingleFunder() &&
|
chanType := channel.ChanType
|
||||||
|
if chanType.IsSingleFunder() && chanType.HasFundingTx() &&
|
||||||
channel.IsInitiator {
|
channel.IsInitiator {
|
||||||
|
|
||||||
err := f.cfg.PublishTransaction(
|
err := f.cfg.PublishTransaction(
|
||||||
@ -1215,6 +1216,7 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
|
|||||||
chainHash := chainhash.Hash(msg.ChainHash)
|
chainHash := chainhash.Hash(msg.ChainHash)
|
||||||
req := &lnwallet.InitFundingReserveMsg{
|
req := &lnwallet.InitFundingReserveMsg{
|
||||||
ChainHash: &chainHash,
|
ChainHash: &chainHash,
|
||||||
|
PendingChanID: msg.PendingChannelID,
|
||||||
NodeID: fmsg.peer.IdentityKey(),
|
NodeID: fmsg.peer.IdentityKey(),
|
||||||
NodeAddr: fmsg.peer.Address(),
|
NodeAddr: fmsg.peer.Address(),
|
||||||
LocalFundingAmt: 0,
|
LocalFundingAmt: 0,
|
||||||
@ -1739,21 +1741,28 @@ func (f *fundingManager) handleFundingSigned(fmsg *fundingSignedMsg) {
|
|||||||
// delete it from our set of active reservations.
|
// delete it from our set of active reservations.
|
||||||
f.deleteReservationCtx(peerKey, pendingChanID)
|
f.deleteReservationCtx(peerKey, pendingChanID)
|
||||||
|
|
||||||
// Broadcast the finalized funding transaction to the network.
|
// Broadcast the finalized funding transaction to the network, but only
|
||||||
|
// if we actually have the funding transaction.
|
||||||
|
if completeChan.ChanType.HasFundingTx() {
|
||||||
fundingTx := completeChan.FundingTxn
|
fundingTx := completeChan.FundingTxn
|
||||||
|
|
||||||
fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v",
|
fndgLog.Infof("Broadcasting funding tx for ChannelPoint(%v): %v",
|
||||||
completeChan.FundingOutpoint, spew.Sdump(fundingTx))
|
completeChan.FundingOutpoint, spew.Sdump(fundingTx))
|
||||||
|
|
||||||
err = f.cfg.PublishTransaction(fundingTx)
|
err = f.cfg.PublishTransaction(fundingTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fndgLog.Errorf("Unable to broadcast funding tx for "+
|
fndgLog.Errorf("Unable to broadcast funding tx for "+
|
||||||
"ChannelPoint(%v): %v", completeChan.FundingOutpoint,
|
"ChannelPoint(%v): %v",
|
||||||
err)
|
completeChan.FundingOutpoint, err)
|
||||||
// We failed to broadcast the funding transaction, but watch
|
|
||||||
// the channel regardless, in case the transaction made it to
|
// We failed to broadcast the funding transaction, but
|
||||||
// the network. We will retry broadcast at startup.
|
// watch the channel regardless, in case the
|
||||||
// TODO(halseth): retry more often? Handle with CPFP? Just
|
// transaction made it to the network. We will retry
|
||||||
// delete from the DB?
|
// broadcast at startup.
|
||||||
|
//
|
||||||
|
// TODO(halseth): retry more often? Handle with CPFP?
|
||||||
|
// Just delete from the DB?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that we have a finalized reservation for this funding flow,
|
// Now that we have a finalized reservation for this funding flow,
|
||||||
@ -2773,6 +2782,10 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
|
|||||||
channelFlags = lnwire.FFAnnounceChannel
|
channelFlags = lnwire.FFAnnounceChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtain a new pending channel ID which is used to track this
|
||||||
|
// reservation throughout its lifetime.
|
||||||
|
chanID := f.nextPendingChanID()
|
||||||
|
|
||||||
// Initialize a funding reservation with the local wallet. If the
|
// Initialize a funding reservation with the local wallet. If the
|
||||||
// wallet doesn't have enough funds to commit to this channel, then the
|
// wallet doesn't have enough funds to commit to this channel, then the
|
||||||
// request will fail, and be aborted.
|
// request will fail, and be aborted.
|
||||||
@ -2790,6 +2803,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
|
|||||||
tweaklessCommitment := localTweakless && remoteTweakless
|
tweaklessCommitment := localTweakless && remoteTweakless
|
||||||
req := &lnwallet.InitFundingReserveMsg{
|
req := &lnwallet.InitFundingReserveMsg{
|
||||||
ChainHash: &msg.chainHash,
|
ChainHash: &msg.chainHash,
|
||||||
|
PendingChanID: chanID,
|
||||||
NodeID: peerKey,
|
NodeID: peerKey,
|
||||||
NodeAddr: msg.peer.Address(),
|
NodeAddr: msg.peer.Address(),
|
||||||
SubtractFees: msg.subtractFees,
|
SubtractFees: msg.subtractFees,
|
||||||
@ -2815,11 +2829,7 @@ func (f *fundingManager) handleInitFundingMsg(msg *initFundingMsg) {
|
|||||||
// SubtractFees=true.
|
// SubtractFees=true.
|
||||||
capacity := reservation.Capacity()
|
capacity := reservation.Capacity()
|
||||||
|
|
||||||
// Obtain a new pending channel ID which is used to track this
|
fndgLog.Infof("Target commit tx sat/kw for pendingID(%x): %v", chanID,
|
||||||
// reservation throughout its lifetime.
|
|
||||||
chanID := f.nextPendingChanID()
|
|
||||||
|
|
||||||
fndgLog.Infof("Target commit tx sat/kw for pending_id(%x): %v", chanID,
|
|
||||||
int64(commitFeePerKw))
|
int64(commitFeePerKw))
|
||||||
|
|
||||||
// If the remote CSV delay was not set in the open channel request,
|
// If the remote CSV delay was not set in the open channel request,
|
||||||
|
@ -2864,7 +2864,7 @@ func TestFundingManagerFundAll(t *testing.T) {
|
|||||||
Value: btcutil.Amount(
|
Value: btcutil.Amount(
|
||||||
0.05 * btcutil.SatoshiPerBitcoin,
|
0.05 * btcutil.SatoshiPerBitcoin,
|
||||||
),
|
),
|
||||||
PkScript: make([]byte, 22),
|
PkScript: coinPkScript,
|
||||||
OutPoint: wire.OutPoint{
|
OutPoint: wire.OutPoint{
|
||||||
Hash: chainhash.Hash{},
|
Hash: chainhash.Hash{},
|
||||||
Index: 0,
|
Index: 0,
|
||||||
@ -2875,7 +2875,7 @@ func TestFundingManagerFundAll(t *testing.T) {
|
|||||||
Value: btcutil.Amount(
|
Value: btcutil.Amount(
|
||||||
0.06 * btcutil.SatoshiPerBitcoin,
|
0.06 * btcutil.SatoshiPerBitcoin,
|
||||||
),
|
),
|
||||||
PkScript: make([]byte, 22),
|
PkScript: coinPkScript,
|
||||||
OutPoint: wire.OutPoint{
|
OutPoint: wire.OutPoint{
|
||||||
Hash: chainhash.Hash{},
|
Hash: chainhash.Hash{},
|
||||||
Index: 1,
|
Index: 1,
|
||||||
|
137
lnwallet/chanfunding/assembler.go
Normal file
137
lnwallet/chanfunding/assembler.go
Normal file
@ -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()
|
||||||
|
}
|
187
lnwallet/chanfunding/canned_assembler.go
Normal file
187
lnwallet/chanfunding/canned_assembler.go
Normal file
@ -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)
|
216
lnwallet/chanfunding/coin_select.go
Normal file
216
lnwallet/chanfunding/coin_select.go
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
package chanfunding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInsufficientFunds is a type matching the error interface which is
|
||||||
|
// returned when coin selection for a new funding transaction fails to due
|
||||||
|
// having an insufficient amount of confirmed funds.
|
||||||
|
type ErrInsufficientFunds struct {
|
||||||
|
amountAvailable btcutil.Amount
|
||||||
|
amountSelected btcutil.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a human readable string describing the error.
|
||||||
|
func (e *ErrInsufficientFunds) Error() string {
|
||||||
|
return fmt.Sprintf("not enough witness outputs to create funding "+
|
||||||
|
"transaction, need %v only have %v available",
|
||||||
|
e.amountAvailable, e.amountSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coin represents a spendable UTXO which is available for channel funding.
|
||||||
|
// This UTXO need not reside in our internal wallet as an example, and instead
|
||||||
|
// may be derived from an existing watch-only wallet. It wraps both the output
|
||||||
|
// present within the UTXO set, and also the outpoint that generates this coin.
|
||||||
|
type Coin struct {
|
||||||
|
wire.TxOut
|
||||||
|
|
||||||
|
wire.OutPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectInputs selects a slice of inputs necessary to meet the specified
|
||||||
|
// selection amount. If input selection is unable to succeed due to insufficient
|
||||||
|
// funds, a non-nil error is returned. Additionally, the total amount of the
|
||||||
|
// selected coins are returned in order for the caller to properly handle
|
||||||
|
// change+fees.
|
||||||
|
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
|
||||||
|
satSelected := btcutil.Amount(0)
|
||||||
|
for i, coin := range coins {
|
||||||
|
satSelected += btcutil.Amount(coin.Value)
|
||||||
|
if satSelected >= amt {
|
||||||
|
return satSelected, coins[:i+1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, &ErrInsufficientFunds{amt, satSelected}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinSelect attempts to select a sufficient amount of coins, including a
|
||||||
|
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
||||||
|
// specified fee rate should be expressed in sat/kw for coin selection to
|
||||||
|
// function properly.
|
||||||
|
func CoinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
||||||
|
coins []Coin) ([]Coin, btcutil.Amount, error) {
|
||||||
|
|
||||||
|
amtNeeded := amt
|
||||||
|
for {
|
||||||
|
// First perform an initial round of coin selection to estimate
|
||||||
|
// the required fee.
|
||||||
|
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var weightEstimate input.TxWeightEstimator
|
||||||
|
|
||||||
|
for _, utxo := range selectedUtxos {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddP2WKHInput()
|
||||||
|
|
||||||
|
case txscript.IsPayToScriptHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddNestedP2WKHInput()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, 0, fmt.Errorf("unsupported address type: %x",
|
||||||
|
utxo.PkScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel funding multisig output is P2WSH.
|
||||||
|
weightEstimate.AddP2WSHOutput()
|
||||||
|
|
||||||
|
// Assume that change output is a P2WKH output.
|
||||||
|
//
|
||||||
|
// TODO: Handle wallets that generate non-witness change
|
||||||
|
// addresses.
|
||||||
|
// TODO(halseth): make coinSelect not estimate change output
|
||||||
|
// for dust change.
|
||||||
|
weightEstimate.AddP2WKHOutput()
|
||||||
|
|
||||||
|
// The difference between the selected amount and the amount
|
||||||
|
// requested will be used to pay fees, and generate a change
|
||||||
|
// output with the remaining.
|
||||||
|
overShootAmt := totalSat - amt
|
||||||
|
|
||||||
|
// Based on the estimated size and fee rate, if the excess
|
||||||
|
// amount isn't enough to pay fees, then increase the requested
|
||||||
|
// coin amount by the estimate required fee, performing another
|
||||||
|
// round of coin selection.
|
||||||
|
totalWeight := int64(weightEstimate.Weight())
|
||||||
|
requiredFee := feeRate.FeeForWeight(totalWeight)
|
||||||
|
if overShootAmt < requiredFee {
|
||||||
|
amtNeeded = amt + requiredFee
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the fee is sufficient, then calculate the size of the
|
||||||
|
// change output.
|
||||||
|
changeAmt := overShootAmt - requiredFee
|
||||||
|
|
||||||
|
return selectedUtxos, changeAmt, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinSelectSubtractFees attempts to select coins such that we'll spend up to
|
||||||
|
// amt in total after fees, adhering to the specified fee rate. The selected
|
||||||
|
// coins, the final output and change values are returned.
|
||||||
|
func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
|
||||||
|
dustLimit btcutil.Amount, coins []Coin) ([]Coin, btcutil.Amount,
|
||||||
|
btcutil.Amount, error) {
|
||||||
|
|
||||||
|
// First perform an initial round of coin selection to estimate
|
||||||
|
// the required fee.
|
||||||
|
totalSat, selectedUtxos, err := selectInputs(amt, coins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var weightEstimate input.TxWeightEstimator
|
||||||
|
for _, utxo := range selectedUtxos {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
case txscript.IsPayToWitnessPubKeyHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddP2WKHInput()
|
||||||
|
|
||||||
|
case txscript.IsPayToScriptHash(utxo.PkScript):
|
||||||
|
weightEstimate.AddNestedP2WKHInput()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, 0, 0, fmt.Errorf("unsupported address "+
|
||||||
|
"type: %x", utxo.PkScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel funding multisig output is P2WSH.
|
||||||
|
weightEstimate.AddP2WSHOutput()
|
||||||
|
|
||||||
|
// At this point we've got two possibilities, either create a
|
||||||
|
// change output, or not. We'll first try without creating a
|
||||||
|
// change output.
|
||||||
|
//
|
||||||
|
// Estimate the fee required for a transaction without a change
|
||||||
|
// output.
|
||||||
|
totalWeight := int64(weightEstimate.Weight())
|
||||||
|
requiredFee := feeRate.FeeForWeight(totalWeight)
|
||||||
|
|
||||||
|
// For a transaction without a change output, we'll let everything go
|
||||||
|
// to our multi-sig output after subtracting fees.
|
||||||
|
outputAmt := totalSat - requiredFee
|
||||||
|
changeAmt := btcutil.Amount(0)
|
||||||
|
|
||||||
|
// If the the output is too small after subtracting the fee, the coin
|
||||||
|
// selection cannot be performed with an amount this small.
|
||||||
|
if outputAmt <= dustLimit {
|
||||||
|
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
|
||||||
|
"subtracting fees(%v) below dust limit(%v)", outputAmt,
|
||||||
|
requiredFee, dustLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We were able to create a transaction with no change from the
|
||||||
|
// selected inputs. We'll remember the resulting values for
|
||||||
|
// now, while we try to add a change output. Assume that change output
|
||||||
|
// is a P2WKH output.
|
||||||
|
weightEstimate.AddP2WKHOutput()
|
||||||
|
|
||||||
|
// Now that we have added the change output, redo the fee
|
||||||
|
// estimate.
|
||||||
|
totalWeight = int64(weightEstimate.Weight())
|
||||||
|
requiredFee = feeRate.FeeForWeight(totalWeight)
|
||||||
|
|
||||||
|
// For a transaction with a change output, everything we don't spend
|
||||||
|
// will go to change.
|
||||||
|
newChange := totalSat - amt
|
||||||
|
newOutput := amt - requiredFee
|
||||||
|
|
||||||
|
// If adding a change output leads to both outputs being above
|
||||||
|
// the dust limit, we'll add the change output. Otherwise we'll
|
||||||
|
// go with the no change tx we originally found.
|
||||||
|
if newChange > dustLimit && newOutput > dustLimit {
|
||||||
|
outputAmt = newOutput
|
||||||
|
changeAmt = newChange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check the resulting output values to make sure we
|
||||||
|
// don't burn a great part to fees.
|
||||||
|
totalOut := outputAmt + changeAmt
|
||||||
|
fee := totalSat - totalOut
|
||||||
|
|
||||||
|
// Fail if more than 20% goes to fees.
|
||||||
|
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
|
||||||
|
// total funding size?
|
||||||
|
if fee > totalOut/5 {
|
||||||
|
return nil, 0, 0, fmt.Errorf("fee %v on total output"+
|
||||||
|
"value %v", fee, totalOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedUtxos, outputAmt, changeAmt, nil
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
package lnwallet
|
package chanfunding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
p2wkhScript, _ = hex.DecodeString(
|
||||||
|
"001411034bdcb6ccb7744fdfdeea958a6fb0b415a032",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// fundingFee is a helper method that returns the fee estimate used for a tx
|
// fundingFee is a helper method that returns the fee estimate used for a tx
|
||||||
// with the given number of inputs and the optional change output. This matches
|
// with the given number of inputs and the optional change output. This matches
|
||||||
// the estimate done by the wallet.
|
// the estimate done by the wallet.
|
||||||
@ -48,7 +56,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
outputValue btcutil.Amount
|
outputValue btcutil.Amount
|
||||||
coins []*Utxo
|
coins []Coin
|
||||||
|
|
||||||
expectedInput []btcutil.Amount
|
expectedInput []btcutil.Amount
|
||||||
expectedChange btcutil.Amount
|
expectedChange btcutil.Amount
|
||||||
@ -61,12 +69,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// This will obviously lead to a change output of
|
// This will obviously lead to a change output of
|
||||||
// almost 0.5 BTC.
|
// almost 0.5 BTC.
|
||||||
name: "big change",
|
name: "big change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
|
outputValue: 0.5 * btcutil.SatoshiPerBitcoin,
|
||||||
|
|
||||||
// The one and only input will be selected.
|
// The one and only input will be selected.
|
||||||
@ -81,12 +91,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// This should lead to an error, as we don't have
|
// This should lead to an error, as we don't have
|
||||||
// enough funds to pay the fee.
|
// enough funds to pay the fee.
|
||||||
name: "nothing left for fees",
|
name: "nothing left for fees",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
outputValue: 1 * btcutil.SatoshiPerBitcoin,
|
outputValue: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
@ -95,12 +107,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// as big as possible, such that the remaining change
|
// as big as possible, such that the remaining change
|
||||||
// will be dust.
|
// will be dust.
|
||||||
name: "dust change",
|
name: "dust change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
// We tune the output value by subtracting the expected
|
// We tune the output value by subtracting the expected
|
||||||
// fee and a small dust amount.
|
// fee and a small dust amount.
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
|
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true) - dust,
|
||||||
@ -117,12 +131,14 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
// as big as possible, such that there is nothing left
|
// as big as possible, such that there is nothing left
|
||||||
// for change.
|
// for change.
|
||||||
name: "no change",
|
name: "no change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
// We tune the output value to be the maximum amount
|
// We tune the output value to be the maximum amount
|
||||||
// possible, leaving just enough for fees.
|
// possible, leaving just enough for fees.
|
||||||
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
|
outputValue: 1*btcutil.SatoshiPerBitcoin - fundingFee(feeRate, 1, true),
|
||||||
@ -143,7 +159,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
selected, changeAmt, err := coinSelect(
|
selected, changeAmt, err := CoinSelect(
|
||||||
feeRate, test.outputValue, test.coins,
|
feeRate, test.outputValue, test.coins,
|
||||||
)
|
)
|
||||||
if !test.expectErr && err != nil {
|
if !test.expectErr && err != nil {
|
||||||
@ -166,7 +182,7 @@ func TestCoinSelect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, coin := range selected {
|
for i, coin := range selected {
|
||||||
if coin.Value != test.expectedInput[i] {
|
if coin.Value != int64(test.expectedInput[i]) {
|
||||||
t.Fatalf("expected input %v to have value %v, "+
|
t.Fatalf("expected input %v to have value %v, "+
|
||||||
"had %v", i, test.expectedInput[i],
|
"had %v", i, test.expectedInput[i],
|
||||||
coin.Value)
|
coin.Value)
|
||||||
@ -195,7 +211,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
spendValue btcutil.Amount
|
spendValue btcutil.Amount
|
||||||
coins []*Utxo
|
coins []Coin
|
||||||
|
|
||||||
expectedInput []btcutil.Amount
|
expectedInput []btcutil.Amount
|
||||||
expectedFundingAmt btcutil.Amount
|
expectedFundingAmt btcutil.Amount
|
||||||
@ -209,12 +225,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// should lead to a funding TX with one output, the
|
// should lead to a funding TX with one output, the
|
||||||
// rest goes to fees.
|
// rest goes to fees.
|
||||||
name: "spend all",
|
name: "spend all",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
spendValue: 1 * btcutil.SatoshiPerBitcoin,
|
spendValue: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
|
|
||||||
// The one and only input will be selected.
|
// The one and only input will be selected.
|
||||||
@ -228,10 +246,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// The total funds available is below the dust limit
|
// The total funds available is below the dust limit
|
||||||
// after paying fees.
|
// after paying fees.
|
||||||
name: "dust output",
|
name: "dust output",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + dust,
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + dust),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dust,
|
spendValue: fundingFee(feeRate, 1, false) + dust,
|
||||||
@ -243,12 +263,14 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// is below the dust limit. The remainder should go
|
// is below the dust limit. The remainder should go
|
||||||
// towards the funding output.
|
// towards the funding output.
|
||||||
name: "dust change",
|
name: "dust change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
|
PkScript: p2wkhScript,
|
||||||
Value: 1 * btcutil.SatoshiPerBitcoin,
|
Value: 1 * btcutil.SatoshiPerBitcoin,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
|
spendValue: 1*btcutil.SatoshiPerBitcoin - dust,
|
||||||
|
|
||||||
expectedInput: []btcutil.Amount{
|
expectedInput: []btcutil.Amount{
|
||||||
@ -260,10 +282,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// We got just enough funds to create an output above the dust limit.
|
// We got just enough funds to create an output above the dust limit.
|
||||||
name: "output right above dustlimit",
|
name: "output right above dustlimit",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + dustLimit + 1),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
||||||
@ -278,10 +302,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
// Amount left is below dust limit after paying fee for
|
// Amount left is below dust limit after paying fee for
|
||||||
// a change output, resulting in a no-change tx.
|
// a change output, resulting in a no-change tx.
|
||||||
name: "no amount to pay fee for change",
|
name: "no amount to pay fee for change",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: fundingFee(feeRate, 1, false) + 2*(dustLimit+1),
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(fundingFee(feeRate, 1, false) + 2*(dustLimit+1)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
spendValue: fundingFee(feeRate, 1, false) + dustLimit + 1,
|
||||||
@ -295,10 +321,12 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// If more than 20% of funds goes to fees, it should fail.
|
// If more than 20% of funds goes to fees, it should fail.
|
||||||
name: "high fee",
|
name: "high fee",
|
||||||
coins: []*Utxo{
|
coins: []Coin{
|
||||||
{
|
{
|
||||||
AddressType: WitnessPubKey,
|
TxOut: wire.TxOut{
|
||||||
Value: 5 * fundingFee(feeRate, 1, false),
|
PkScript: p2wkhScript,
|
||||||
|
Value: int64(5 * fundingFee(feeRate, 1, false)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spendValue: 5 * fundingFee(feeRate, 1, false),
|
spendValue: 5 * fundingFee(feeRate, 1, false),
|
||||||
@ -308,8 +336,10 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
selected, localFundingAmt, changeAmt, err := coinSelectSubtractFees(
|
selected, localFundingAmt, changeAmt, err := CoinSelectSubtractFees(
|
||||||
feeRate, test.spendValue, dustLimit, test.coins,
|
feeRate, test.spendValue, dustLimit, test.coins,
|
||||||
)
|
)
|
||||||
if !test.expectErr && err != nil {
|
if !test.expectErr && err != nil {
|
||||||
@ -332,7 +362,7 @@ func TestCoinSelectSubtractFees(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, coin := range selected {
|
for i, coin := range selected {
|
||||||
if coin.Value != test.expectedInput[i] {
|
if coin.Value != int64(test.expectedInput[i]) {
|
||||||
t.Fatalf("expected input %v to have value %v, "+
|
t.Fatalf("expected input %v to have value %v, "+
|
||||||
"had %v", i, test.expectedInput[i],
|
"had %v", i, test.expectedInput[i],
|
||||||
coin.Value)
|
coin.Value)
|
29
lnwallet/chanfunding/log.go
Normal file
29
lnwallet/chanfunding/log.go
Normal file
@ -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
|
||||||
|
}
|
343
lnwallet/chanfunding/wallet_assembler.go
Normal file
343
lnwallet/chanfunding/wallet_assembler.go
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
package chanfunding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/btcsuite/btcutil/txsort"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FullIntent is an intent that is fully backed by the internal wallet. This
|
||||||
|
// intent differs from the ShimIntent, in that the funding transaction will be
|
||||||
|
// constructed internally, and will consist of only inputs we wholly control.
|
||||||
|
// This Intent implements a basic state machine that must be executed in order
|
||||||
|
// before CompileFundingTx can be called.
|
||||||
|
//
|
||||||
|
// Steps to final channel provisioning:
|
||||||
|
// 1. Call BindKeys to notify the intent which keys to use when constructing
|
||||||
|
// the multi-sig output.
|
||||||
|
// 2. Call CompileFundingTx afterwards to obtain the funding transaction.
|
||||||
|
//
|
||||||
|
// If either of these steps fail, then the Cancel method MUST be called.
|
||||||
|
type FullIntent struct {
|
||||||
|
ShimIntent
|
||||||
|
|
||||||
|
// InputCoins are the set of coins selected as inputs to this funding
|
||||||
|
// transaction.
|
||||||
|
InputCoins []Coin
|
||||||
|
|
||||||
|
// ChangeOutputs are the set of outputs that the Assembler will use as
|
||||||
|
// change from the main funding transaction.
|
||||||
|
ChangeOutputs []*wire.TxOut
|
||||||
|
|
||||||
|
// coinLocker is the Assembler's instance of the OutpointLocker
|
||||||
|
// interface.
|
||||||
|
coinLocker OutpointLocker
|
||||||
|
|
||||||
|
// coinSource is the Assembler's instance of the CoinSource interface.
|
||||||
|
coinSource CoinSource
|
||||||
|
|
||||||
|
// signer is the Assembler's instance of the Singer interface.
|
||||||
|
signer input.Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindKeys is a method unique to the FullIntent variant. This allows the
|
||||||
|
// caller to decide precisely which keys are used in the final funding
|
||||||
|
// transaction. This is kept out of the main Assembler as these may may not
|
||||||
|
// necessarily be under full control of the wallet. Only after this method has
|
||||||
|
// been executed will CompileFundingTx succeed.
|
||||||
|
func (f *FullIntent) BindKeys(localKey *keychain.KeyDescriptor,
|
||||||
|
remoteKey *btcec.PublicKey) {
|
||||||
|
|
||||||
|
f.localKey = localKey
|
||||||
|
f.remoteKey = remoteKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompileFundingTx is to be called after BindKeys on the sub-intent has been
|
||||||
|
// called. This method will construct the final funding transaction, and fully
|
||||||
|
// sign all inputs that are known by the backing CoinSource. After this method
|
||||||
|
// returns, the Intent is assumed to be complete, as the output can be created
|
||||||
|
// at any point.
|
||||||
|
func (f *FullIntent) CompileFundingTx(extraInputs []*wire.TxIn,
|
||||||
|
extraOutputs []*wire.TxOut) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
|
// Create a blank, fresh transaction. Soon to be a complete funding
|
||||||
|
// transaction which will allow opening a lightning channel.
|
||||||
|
fundingTx := wire.NewMsgTx(2)
|
||||||
|
|
||||||
|
// Add all multi-party inputs and outputs to the transaction.
|
||||||
|
for _, coin := range f.InputCoins {
|
||||||
|
fundingTx.AddTxIn(&wire.TxIn{
|
||||||
|
PreviousOutPoint: coin.OutPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, theirInput := range extraInputs {
|
||||||
|
fundingTx.AddTxIn(theirInput)
|
||||||
|
}
|
||||||
|
for _, ourChangeOutput := range f.ChangeOutputs {
|
||||||
|
fundingTx.AddTxOut(ourChangeOutput)
|
||||||
|
}
|
||||||
|
for _, theirChangeOutput := range extraOutputs {
|
||||||
|
fundingTx.AddTxOut(theirChangeOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, fundingOutput, err := f.FundingOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the transaction. Since both side agree to a canonical ordering,
|
||||||
|
// by sorting we no longer need to send the entire transaction. Only
|
||||||
|
// signatures will be exchanged.
|
||||||
|
fundingTx.AddTxOut(fundingOutput)
|
||||||
|
txsort.InPlaceSort(fundingTx)
|
||||||
|
|
||||||
|
// Now that the funding tx has been fully assembled, we'll locate the
|
||||||
|
// index of the funding output so we can create our final channel
|
||||||
|
// point.
|
||||||
|
_, multiSigIndex := input.FindScriptOutputIndex(
|
||||||
|
fundingTx, fundingOutput.PkScript,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Next, sign all inputs that are ours, collecting the signatures in
|
||||||
|
// order of the inputs.
|
||||||
|
signDesc := input.SignDescriptor{
|
||||||
|
HashType: txscript.SigHashAll,
|
||||||
|
SigHashes: txscript.NewTxSigHashes(fundingTx),
|
||||||
|
}
|
||||||
|
for i, txIn := range fundingTx.TxIn {
|
||||||
|
// We can only sign this input if it's ours, so we'll ask the
|
||||||
|
// coin source if it can map this outpoint into a coin we own.
|
||||||
|
// If not, then we'll continue as it isn't our input.
|
||||||
|
info, err := f.coinSource.CoinFromOutPoint(
|
||||||
|
txIn.PreviousOutPoint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we know the input is ours, we'll populate the
|
||||||
|
// signDesc with the per input unique information.
|
||||||
|
signDesc.Output = &wire.TxOut{
|
||||||
|
Value: info.Value,
|
||||||
|
PkScript: info.PkScript,
|
||||||
|
}
|
||||||
|
signDesc.InputIndex = i
|
||||||
|
|
||||||
|
// Finally, we'll sign the input as is, and populate the input
|
||||||
|
// with the witness and sigScript (if needed).
|
||||||
|
inputScript, err := f.signer.ComputeInputScript(
|
||||||
|
fundingTx, &signDesc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
txIn.SignatureScript = inputScript.SigScript
|
||||||
|
txIn.Witness = inputScript.Witness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll populate the chanPoint now that we've fully
|
||||||
|
// constructed the funding transaction.
|
||||||
|
f.chanPoint = &wire.OutPoint{
|
||||||
|
Hash: fundingTx.TxHash(),
|
||||||
|
Index: multiSigIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fundingTx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel allows the caller to cancel a funding Intent at any time. This will
|
||||||
|
// return any resources such as coins back to the eligible pool to be used in
|
||||||
|
// order channel fundings.
|
||||||
|
//
|
||||||
|
// NOTE: Part of the chanfunding.Intent interface.
|
||||||
|
func (f *FullIntent) Cancel() {
|
||||||
|
for _, coin := range f.InputCoins {
|
||||||
|
f.coinLocker.UnlockOutpoint(coin.OutPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.ShimIntent.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile-time check to ensure FullIntent meets the Intent interface.
|
||||||
|
var _ Intent = (*FullIntent)(nil)
|
||||||
|
|
||||||
|
// WalletConfig is the main config of the WalletAssembler.
|
||||||
|
type WalletConfig struct {
|
||||||
|
// CoinSource is what the WalletAssembler uses to list/locate coins.
|
||||||
|
CoinSource CoinSource
|
||||||
|
|
||||||
|
// CoinSelectionLocker allows the WalletAssembler to gain exclusive
|
||||||
|
// access to the current set of coins returned by the CoinSource.
|
||||||
|
CoinSelectLocker CoinSelectionLocker
|
||||||
|
|
||||||
|
// CoinLocker is what the WalletAssembler uses to lock coins that may
|
||||||
|
// be used as inputs for a new funding transaction.
|
||||||
|
CoinLocker OutpointLocker
|
||||||
|
|
||||||
|
// Signer allows the WalletAssembler to sign inputs on any potential
|
||||||
|
// funding transactions.
|
||||||
|
Signer input.Signer
|
||||||
|
|
||||||
|
// DustLimit is the current dust limit. We'll use this to ensure that
|
||||||
|
// we don't make dust outputs on the funding transaction.
|
||||||
|
DustLimit btcutil.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalletAssembler is an instance of the Assembler interface that is backed by
|
||||||
|
// a full wallet. This variant of the Assembler interface will produce the
|
||||||
|
// entirety of the funding transaction within the wallet. This implements the
|
||||||
|
// typical funding flow that is initiated either on the p2p level or using the
|
||||||
|
// CLi.
|
||||||
|
type WalletAssembler struct {
|
||||||
|
cfg WalletConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWalletAssembler creates a new instance of the WalletAssembler from a
|
||||||
|
// fully populated wallet config.
|
||||||
|
func NewWalletAssembler(cfg WalletConfig) *WalletAssembler {
|
||||||
|
return &WalletAssembler{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisionChannel is the main entry point to begin a funding workflow given a
|
||||||
|
// fully populated request. The internal WalletAssembler will perform coin
|
||||||
|
// selection in a goroutine safe manner, returning an Intent that will allow
|
||||||
|
// the caller to finalize the funding process.
|
||||||
|
//
|
||||||
|
// NOTE: To cancel the funding flow the Cancel() method on the returned Intent,
|
||||||
|
// MUST be called.
|
||||||
|
//
|
||||||
|
// NOTE: This is a part of the chanfunding.Assembler interface.
|
||||||
|
func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
|
||||||
|
var intent Intent
|
||||||
|
|
||||||
|
// We hold the coin select mutex while querying for outputs, and
|
||||||
|
// performing coin selection in order to avoid inadvertent double
|
||||||
|
// spends across funding transactions.
|
||||||
|
err := w.cfg.CoinSelectLocker.WithCoinSelectLock(func() error {
|
||||||
|
log.Infof("Performing funding tx coin selection using %v "+
|
||||||
|
"sat/kw as fee rate", int64(r.FeeRate))
|
||||||
|
|
||||||
|
// Find all unlocked unspent witness outputs that satisfy the
|
||||||
|
// minimum number of confirmations required.
|
||||||
|
coins, err := w.cfg.CoinSource.ListCoins(
|
||||||
|
r.MinConfs, math.MaxInt32,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
selectedCoins []Coin
|
||||||
|
localContributionAmt btcutil.Amount
|
||||||
|
changeAmt btcutil.Amount
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform coin selection over our available, unlocked unspent
|
||||||
|
// outputs in order to find enough coins to meet the funding
|
||||||
|
// amount requirements.
|
||||||
|
switch {
|
||||||
|
// If there's no funding amount at all (receiving an inbound
|
||||||
|
// single funder request), then we don't need to perform any
|
||||||
|
// coin selection at all.
|
||||||
|
case r.LocalAmt == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
// In case this request want the fees subtracted from the local
|
||||||
|
// amount, we'll call the specialized method for that. This
|
||||||
|
// ensures that we won't deduct more that the specified balance
|
||||||
|
// from our wallet.
|
||||||
|
case r.SubtractFees:
|
||||||
|
dustLimit := w.cfg.DustLimit
|
||||||
|
selectedCoins, localContributionAmt, changeAmt, err = CoinSelectSubtractFees(
|
||||||
|
r.FeeRate, r.LocalAmt, dustLimit, coins,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise do a normal coin selection where we target a given
|
||||||
|
// funding amount.
|
||||||
|
default:
|
||||||
|
localContributionAmt = r.LocalAmt
|
||||||
|
selectedCoins, changeAmt, err = CoinSelect(
|
||||||
|
r.FeeRate, r.LocalAmt, coins,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record any change output(s) generated as a result of the
|
||||||
|
// coin selection, but only if the addition of the output won't
|
||||||
|
// lead to the creation of dust.
|
||||||
|
var changeOutput *wire.TxOut
|
||||||
|
if changeAmt != 0 && changeAmt > w.cfg.DustLimit {
|
||||||
|
changeAddr, err := r.ChangeAddr()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
changeScript, err := txscript.PayToAddrScript(changeAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
changeOutput = &wire.TxOut{
|
||||||
|
Value: int64(changeAmt),
|
||||||
|
PkScript: changeScript,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the selected coins. These coins are now "reserved",
|
||||||
|
// this prevents concurrent funding requests from referring to
|
||||||
|
// and this double-spending the same set of coins.
|
||||||
|
for _, coin := range selectedCoins {
|
||||||
|
outpoint := coin.OutPoint
|
||||||
|
|
||||||
|
w.cfg.CoinLocker.LockOutpoint(outpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
newIntent := &FullIntent{
|
||||||
|
ShimIntent: ShimIntent{
|
||||||
|
localFundingAmt: localContributionAmt,
|
||||||
|
remoteFundingAmt: r.RemoteAmt,
|
||||||
|
},
|
||||||
|
InputCoins: selectedCoins,
|
||||||
|
coinLocker: w.cfg.CoinLocker,
|
||||||
|
coinSource: w.cfg.CoinSource,
|
||||||
|
signer: w.cfg.Signer,
|
||||||
|
}
|
||||||
|
|
||||||
|
if changeOutput != nil {
|
||||||
|
newIntent.ChangeOutputs = []*wire.TxOut{changeOutput}
|
||||||
|
}
|
||||||
|
|
||||||
|
intent = newIntent
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return intent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FundingTxAvailable is an empty method that an assembler can implement to
|
||||||
|
// signal to callers that its able to provide the funding transaction for the
|
||||||
|
// channel via the intent it returns.
|
||||||
|
//
|
||||||
|
// NOTE: This method is a part of the FundingTxAssembler interface.
|
||||||
|
func (w *WalletAssembler) FundingTxAvailable() {}
|
||||||
|
|
||||||
|
// A compile-time assertion to ensure the WalletAssembler meets the
|
||||||
|
// FundingTxAssembler interface.
|
||||||
|
var _ FundingTxAssembler = (*WalletAssembler)(nil)
|
@ -41,6 +41,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -616,7 +617,7 @@ func testFundingTransactionLockedOutputs(miner *rpctest.Harness,
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("not error returned, should fail on coin selection")
|
t.Fatalf("not error returned, should fail on coin selection")
|
||||||
}
|
}
|
||||||
if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok {
|
if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok {
|
||||||
t.Fatalf("error not coinselect error: %v", err)
|
t.Fatalf("error not coinselect error: %v", err)
|
||||||
}
|
}
|
||||||
if failedReservation != nil {
|
if failedReservation != nil {
|
||||||
@ -655,7 +656,7 @@ func testFundingCancellationNotEnoughFunds(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// Attempt to create another channel with 44 BTC, this should fail.
|
// Attempt to create another channel with 44 BTC, this should fail.
|
||||||
_, err = alice.InitChannelReservation(req)
|
_, err = alice.InitChannelReservation(req)
|
||||||
if _, ok := err.(*lnwallet.ErrInsufficientFunds); !ok {
|
if _, ok := err.(*chanfunding.ErrInsufficientFunds); !ok {
|
||||||
t.Fatalf("coin selection succeeded should have insufficient funds: %v",
|
t.Fatalf("coin selection succeeded should have insufficient funds: %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
@ -699,7 +700,7 @@ func testCancelNonExistentReservation(miner *rpctest.Harness,
|
|||||||
// Create our own reservation, give it some ID.
|
// Create our own reservation, give it some ID.
|
||||||
res, err := lnwallet.NewChannelReservation(
|
res, err := lnwallet.NewChannelReservation(
|
||||||
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
|
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
|
||||||
lnwire.FFAnnounceChannel, true,
|
lnwire.FFAnnounceChannel, true, nil, [32]byte{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create res: %v", err)
|
t.Fatalf("unable to create res: %v", err)
|
||||||
@ -792,7 +793,9 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
||||||
alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool) {
|
alice, bob *lnwallet.LightningWallet, t *testing.T, tweakless bool,
|
||||||
|
aliceChanFunder chanfunding.Assembler,
|
||||||
|
fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) {
|
||||||
|
|
||||||
// For this scenario, Alice will be the channel initiator while bob
|
// For this scenario, Alice will be the channel initiator while bob
|
||||||
// will act as the responder to the workflow.
|
// will act as the responder to the workflow.
|
||||||
@ -811,6 +814,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
}
|
}
|
||||||
aliceReq := &lnwallet.InitFundingReserveMsg{
|
aliceReq := &lnwallet.InitFundingReserveMsg{
|
||||||
ChainHash: chainHash,
|
ChainHash: chainHash,
|
||||||
|
PendingChanID: pendingChanID,
|
||||||
NodeID: bobPub,
|
NodeID: bobPub,
|
||||||
NodeAddr: bobAddr,
|
NodeAddr: bobAddr,
|
||||||
LocalFundingAmt: fundingAmt,
|
LocalFundingAmt: fundingAmt,
|
||||||
@ -820,6 +824,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
PushMSat: pushAmt,
|
PushMSat: pushAmt,
|
||||||
Flags: lnwire.FFAnnounceChannel,
|
Flags: lnwire.FFAnnounceChannel,
|
||||||
Tweakless: tweakless,
|
Tweakless: tweakless,
|
||||||
|
ChanFunder: aliceChanFunder,
|
||||||
}
|
}
|
||||||
aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
|
aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -839,15 +844,20 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
t.Fatalf("unable to verify constraints: %v", err)
|
t.Fatalf("unable to verify constraints: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify all contribution fields have been set properly.
|
// Verify all contribution fields have been set properly, but only if
|
||||||
|
// Alice is the funder herself.
|
||||||
aliceContribution := aliceChanReservation.OurContribution()
|
aliceContribution := aliceChanReservation.OurContribution()
|
||||||
|
if fetchFundingTx == nil {
|
||||||
if len(aliceContribution.Inputs) < 1 {
|
if len(aliceContribution.Inputs) < 1 {
|
||||||
t.Fatalf("outputs for funding tx not properly selected, have %v "+
|
t.Fatalf("outputs for funding tx not properly "+
|
||||||
"outputs should at least 1", len(aliceContribution.Inputs))
|
"selected, have %v outputs should at least 1",
|
||||||
|
len(aliceContribution.Inputs))
|
||||||
}
|
}
|
||||||
if len(aliceContribution.ChangeOutputs) != 1 {
|
if len(aliceContribution.ChangeOutputs) != 1 {
|
||||||
t.Fatalf("coin selection failed, should have one change outputs, "+
|
t.Fatalf("coin selection failed, should have one "+
|
||||||
"instead have: %v", len(aliceContribution.ChangeOutputs))
|
"change outputs, instead have: %v",
|
||||||
|
len(aliceContribution.ChangeOutputs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assertContributionInitPopulated(t, aliceContribution)
|
assertContributionInitPopulated(t, aliceContribution)
|
||||||
|
|
||||||
@ -855,6 +865,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
// reservation initiation, then consume Alice's contribution.
|
// reservation initiation, then consume Alice's contribution.
|
||||||
bobReq := &lnwallet.InitFundingReserveMsg{
|
bobReq := &lnwallet.InitFundingReserveMsg{
|
||||||
ChainHash: chainHash,
|
ChainHash: chainHash,
|
||||||
|
PendingChanID: pendingChanID,
|
||||||
NodeID: alicePub,
|
NodeID: alicePub,
|
||||||
NodeAddr: aliceAddr,
|
NodeAddr: aliceAddr,
|
||||||
LocalFundingAmt: 0,
|
LocalFundingAmt: 0,
|
||||||
@ -897,10 +908,11 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// At this point, Alice should have generated all the signatures
|
// At this point, Alice should have generated all the signatures
|
||||||
// required for the funding transaction, as well as Alice's commitment
|
// required for the funding transaction, as well as Alice's commitment
|
||||||
// signature to bob.
|
// signature to bob, but only if the funding transaction was
|
||||||
|
// constructed internally.
|
||||||
aliceRemoteContribution := aliceChanReservation.TheirContribution()
|
aliceRemoteContribution := aliceChanReservation.TheirContribution()
|
||||||
aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures()
|
aliceFundingSigs, aliceCommitSig := aliceChanReservation.OurSignatures()
|
||||||
if aliceFundingSigs == nil {
|
if fetchFundingTx == nil && aliceFundingSigs == nil {
|
||||||
t.Fatalf("funding sigs not found")
|
t.Fatalf("funding sigs not found")
|
||||||
}
|
}
|
||||||
if aliceCommitSig == nil {
|
if aliceCommitSig == nil {
|
||||||
@ -909,7 +921,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
|
|
||||||
// Additionally, the funding tx and the funding outpoint should have
|
// Additionally, the funding tx and the funding outpoint should have
|
||||||
// been populated.
|
// been populated.
|
||||||
if aliceChanReservation.FinalFundingTx() == nil {
|
if aliceChanReservation.FinalFundingTx() == nil && fetchFundingTx == nil {
|
||||||
t.Fatalf("funding transaction never created!")
|
t.Fatalf("funding transaction never created!")
|
||||||
}
|
}
|
||||||
if aliceChanReservation.FundingOutpoint() == nil {
|
if aliceChanReservation.FundingOutpoint() == nil {
|
||||||
@ -951,9 +963,17 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
|
|||||||
t.Fatalf("alice unable to complete reservation: %v", err)
|
t.Fatalf("alice unable to complete reservation: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the caller provided an alternative way to obtain the funding tx,
|
||||||
|
// then we'll use that. Otherwise, we'll obtain it directly from Alice.
|
||||||
|
var fundingTx *wire.MsgTx
|
||||||
|
if fetchFundingTx != nil {
|
||||||
|
fundingTx = fetchFundingTx()
|
||||||
|
} else {
|
||||||
|
fundingTx = aliceChanReservation.FinalFundingTx()
|
||||||
|
}
|
||||||
|
|
||||||
// The resulting active channel state should have been persisted to the
|
// The resulting active channel state should have been persisted to the
|
||||||
// DB for both Alice and Bob.
|
// DB for both Alice and Bob.
|
||||||
fundingTx := aliceChanReservation.FinalFundingTx()
|
|
||||||
fundingSha := fundingTx.TxHash()
|
fundingSha := fundingTx.TxHash()
|
||||||
aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub)
|
aliceChannels, err := alice.Cfg.Database.FetchOpenChannels(bobPub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2523,7 +2543,8 @@ var walletTests = []walletTestCase{
|
|||||||
bob *lnwallet.LightningWallet, t *testing.T) {
|
bob *lnwallet.LightningWallet, t *testing.T) {
|
||||||
|
|
||||||
testSingleFunderReservationWorkflow(
|
testSingleFunderReservationWorkflow(
|
||||||
miner, alice, bob, t, false,
|
miner, alice, bob, t, false, nil, nil,
|
||||||
|
[32]byte{},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -2533,10 +2554,15 @@ var walletTests = []walletTestCase{
|
|||||||
bob *lnwallet.LightningWallet, t *testing.T) {
|
bob *lnwallet.LightningWallet, t *testing.T) {
|
||||||
|
|
||||||
testSingleFunderReservationWorkflow(
|
testSingleFunderReservationWorkflow(
|
||||||
miner, alice, bob, t, true,
|
miner, alice, bob, t, true, nil, nil,
|
||||||
|
[32]byte{},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "single funding workflow external funding tx",
|
||||||
|
test: testSingleFunderExternalFundingTx,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "dual funder workflow",
|
name: "dual funder workflow",
|
||||||
test: testDualFundingReservationWorkflow,
|
test: testDualFundingReservationWorkflow,
|
||||||
@ -2675,6 +2701,114 @@ func waitForWalletSync(r *rpctest.Harness, w *lnwallet.LightningWallet) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testSingleFunderExternalFundingTx tests that the wallet is able to properly
|
||||||
|
// carry out a funding flow backed by a channel point that has been crafted
|
||||||
|
// outside the wallet.
|
||||||
|
func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
|
||||||
|
alice, bob *lnwallet.LightningWallet, t *testing.T) {
|
||||||
|
|
||||||
|
// First, we'll obtain multi-sig keys from both Alice and Bob which
|
||||||
|
// simulates them exchanging keys on a higher level.
|
||||||
|
aliceFundingKey, err := alice.DeriveNextKey(keychain.KeyFamilyMultiSig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to obtain alice funding key: %v", err)
|
||||||
|
}
|
||||||
|
bobFundingKey, err := bob.DeriveNextKey(keychain.KeyFamilyMultiSig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to obtain bob funding key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now set up for them to open a 4 BTC channel, with 1 BTC pushed
|
||||||
|
// to Bob's side.
|
||||||
|
chanAmt := 4 * btcutil.SatoshiPerBitcoin
|
||||||
|
|
||||||
|
// Simulating external funding negotiation, we'll now create the
|
||||||
|
// funding transaction for both parties. Utilizing existing tools,
|
||||||
|
// we'll create a new chanfunding.Assembler hacked by Alice's wallet.
|
||||||
|
aliceChanFunder := chanfunding.NewWalletAssembler(chanfunding.WalletConfig{
|
||||||
|
CoinSource: lnwallet.NewCoinSource(alice),
|
||||||
|
CoinSelectLocker: alice,
|
||||||
|
CoinLocker: alice,
|
||||||
|
Signer: alice.Cfg.Signer,
|
||||||
|
DustLimit: 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
// With the chan funder created, we'll now provision a funding intent,
|
||||||
|
// bind the keys we obtained above, and finally obtain our funding
|
||||||
|
// transaction and outpoint.
|
||||||
|
fundingIntent, err := aliceChanFunder.ProvisionChannel(&chanfunding.Request{
|
||||||
|
LocalAmt: btcutil.Amount(chanAmt),
|
||||||
|
MinConfs: 1,
|
||||||
|
FeeRate: 253,
|
||||||
|
ChangeAddr: func() (btcutil.Address, error) {
|
||||||
|
return alice.NewAddress(lnwallet.WitnessPubKey, true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to perform coin selection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With our intent created, we'll instruct it to finalize the funding
|
||||||
|
// transaction, and also hand us the outpoint so we can simulate
|
||||||
|
// external crafting of the funding transaction.
|
||||||
|
var (
|
||||||
|
fundingTx *wire.MsgTx
|
||||||
|
chanPoint *wire.OutPoint
|
||||||
|
)
|
||||||
|
if fullIntent, ok := fundingIntent.(*chanfunding.FullIntent); ok {
|
||||||
|
fullIntent.BindKeys(&aliceFundingKey, bobFundingKey.PubKey)
|
||||||
|
|
||||||
|
fundingTx, err = fullIntent.CompileFundingTx(nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to compile funding tx: %v", err)
|
||||||
|
}
|
||||||
|
chanPoint, err = fullIntent.ChanPoint()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to obtain chan point: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected full intent, instead got: %T", fullIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have the fully constructed funding transaction, we'll
|
||||||
|
// create a new shim external funder out of it for Alice, and prep a
|
||||||
|
// shim intent for Bob.
|
||||||
|
aliceExternalFunder := chanfunding.NewCannedAssembler(
|
||||||
|
*chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey,
|
||||||
|
bobFundingKey.PubKey, true,
|
||||||
|
)
|
||||||
|
bobShimIntent, err := chanfunding.NewCannedAssembler(
|
||||||
|
*chanPoint, btcutil.Amount(chanAmt), &bobFundingKey,
|
||||||
|
aliceFundingKey.PubKey, false,
|
||||||
|
).ProvisionChannel(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create shim intent for bob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have everything we need to carry out our test, so
|
||||||
|
// we'll being the funding flow between Alice and Bob.
|
||||||
|
//
|
||||||
|
// However, before we do so, we'll register a new shim intent for Bob,
|
||||||
|
// so he knows what keys to use when he receives the funding request
|
||||||
|
// from Alice.
|
||||||
|
pendingChanID := testHdSeed
|
||||||
|
err = bob.RegisterFundingIntent(pendingChanID, bobShimIntent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to register intent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can carry out the single funding flow as normal, we'll
|
||||||
|
// specify our external funder and funding transaction, as well as the
|
||||||
|
// pending channel ID generated above to allow Alice and Bob to track
|
||||||
|
// the funding flow externally.
|
||||||
|
testSingleFunderReservationWorkflow(
|
||||||
|
miner, alice, bob, t, true, aliceExternalFunder,
|
||||||
|
func() *wire.MsgTx {
|
||||||
|
return fundingTx
|
||||||
|
}, pendingChanID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TestInterfaces tests all registered interfaces with a unified set of tests
|
// TestInterfaces tests all registered interfaces with a unified set of tests
|
||||||
// which exercise each of the required methods found within the WalletController
|
// which exercise each of the required methods found within the WalletController
|
||||||
// interface.
|
// interface.
|
||||||
@ -2737,8 +2871,10 @@ func TestLightningWallet(t *testing.T) {
|
|||||||
|
|
||||||
for _, walletDriver := range lnwallet.RegisteredWallets() {
|
for _, walletDriver := range lnwallet.RegisteredWallets() {
|
||||||
for _, backEnd := range walletDriver.BackEnds() {
|
for _, backEnd := range walletDriver.BackEnds() {
|
||||||
runTests(t, walletDriver, backEnd, miningNode,
|
if !runTests(t, walletDriver, backEnd, miningNode,
|
||||||
rpcConfig, chainNotifier)
|
rpcConfig, chainNotifier) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2750,7 +2886,7 @@ func TestLightningWallet(t *testing.T) {
|
|||||||
func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||||
backEnd string, miningNode *rpctest.Harness,
|
backEnd string, miningNode *rpctest.Harness,
|
||||||
rpcConfig rpcclient.ConnConfig,
|
rpcConfig rpcclient.ConnConfig,
|
||||||
chainNotifier *btcdnotify.BtcdNotifier) {
|
chainNotifier chainntnfs.ChainNotifier) bool {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bio lnwallet.BlockChainIO
|
bio lnwallet.BlockChainIO
|
||||||
@ -2979,8 +3115,7 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
|||||||
|
|
||||||
bob, err := createTestWallet(
|
bob, err := createTestWallet(
|
||||||
tempTestDirBob, miningNode, netParams,
|
tempTestDirBob, miningNode, netParams,
|
||||||
chainNotifier, bobWalletController, bobKeyRing,
|
chainNotifier, bobWalletController, bobKeyRing, bobSigner, bio,
|
||||||
bobSigner, bio,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test ln wallet: %v", err)
|
t.Fatalf("unable to create test ln wallet: %v", err)
|
||||||
@ -2995,13 +3130,22 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
|||||||
// Execute every test, clearing possibly mutated
|
// Execute every test, clearing possibly mutated
|
||||||
// wallet state after each step.
|
// wallet state after each step.
|
||||||
for _, walletTest := range walletTests {
|
for _, walletTest := range walletTests {
|
||||||
|
|
||||||
|
walletTest := walletTest
|
||||||
|
|
||||||
testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd,
|
testName := fmt.Sprintf("%v/%v:%v", walletType, backEnd,
|
||||||
walletTest.name)
|
walletTest.name)
|
||||||
success := t.Run(testName, func(t *testing.T) {
|
success := t.Run(testName, func(t *testing.T) {
|
||||||
|
if backEnd == "neutrino" &&
|
||||||
|
strings.Contains(walletTest.name, "dual funder") {
|
||||||
|
t.Skip("skipping dual funder tests for neutrino")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
walletTest.test(miningNode, alice, bob, t)
|
walletTest.test(miningNode, alice, bob, t)
|
||||||
})
|
})
|
||||||
if !success {
|
if !success {
|
||||||
break
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): possible reset mining
|
// TODO(roasbeef): possible reset mining
|
||||||
@ -3012,4 +3156,6 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
|||||||
t.Fatalf("unable to wipe wallet state: %v", err)
|
t.Fatalf("unable to wipe wallet state: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -109,19 +110,19 @@ type ChannelReservation struct {
|
|||||||
// throughout its lifetime.
|
// throughout its lifetime.
|
||||||
reservationID uint64
|
reservationID uint64
|
||||||
|
|
||||||
|
// pendingChanID is the pending channel ID for this channel as
|
||||||
|
// identified within the wire protocol.
|
||||||
|
pendingChanID [32]byte
|
||||||
|
|
||||||
// pushMSat the amount of milli-satoshis that should be pushed to the
|
// pushMSat the amount of milli-satoshis that should be pushed to the
|
||||||
// responder of a single funding channel as part of the initial
|
// responder of a single funding channel as part of the initial
|
||||||
// commitment state.
|
// commitment state.
|
||||||
pushMSat lnwire.MilliSatoshi
|
pushMSat lnwire.MilliSatoshi
|
||||||
|
|
||||||
// chanOpen houses a struct containing the channel and additional
|
|
||||||
// confirmation details will be sent on once the channel is considered
|
|
||||||
// 'open'. A channel is open once the funding transaction has reached a
|
|
||||||
// sufficient number of confirmations.
|
|
||||||
chanOpen chan *openChanDetails
|
|
||||||
chanOpenErr chan error
|
|
||||||
|
|
||||||
wallet *LightningWallet
|
wallet *LightningWallet
|
||||||
|
chanFunder chanfunding.Assembler
|
||||||
|
|
||||||
|
fundingIntent chanfunding.Intent
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelReservation creates a new channel reservation. This function is
|
// NewChannelReservation creates a new channel reservation. This function is
|
||||||
@ -131,8 +132,9 @@ type ChannelReservation struct {
|
|||||||
func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
||||||
commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet,
|
commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet,
|
||||||
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
|
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
|
||||||
flags lnwire.FundingFlag,
|
flags lnwire.FundingFlag, tweaklessCommit bool,
|
||||||
tweaklessCommit bool) (*ChannelReservation, error) {
|
fundingAssembler chanfunding.Assembler,
|
||||||
|
pendingChanID [32]byte) (*ChannelReservation, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ourBalance lnwire.MilliSatoshi
|
ourBalance lnwire.MilliSatoshi
|
||||||
@ -220,6 +222,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
|||||||
} else {
|
} else {
|
||||||
chanType |= channeldb.SingleFunderBit
|
chanType |= channeldb.SingleFunderBit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this intent isn't one that's able to provide us with a
|
||||||
|
// funding transaction, then we'll set the chanType bit to
|
||||||
|
// signal that we don't have access to one.
|
||||||
|
if _, ok := fundingAssembler.(chanfunding.FundingTxAssembler); !ok {
|
||||||
|
chanType |= channeldb.NoFundingTxBit
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, this is a dual funder channel, and no side is
|
// Otherwise, this is a dual funder channel, and no side is
|
||||||
// technically the "initiator"
|
// technically the "initiator"
|
||||||
@ -258,10 +268,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
|||||||
Db: wallet.Cfg.Database,
|
Db: wallet.Cfg.Database,
|
||||||
},
|
},
|
||||||
pushMSat: pushMSat,
|
pushMSat: pushMSat,
|
||||||
|
pendingChanID: pendingChanID,
|
||||||
reservationID: id,
|
reservationID: id,
|
||||||
chanOpen: make(chan *openChanDetails, 1),
|
|
||||||
chanOpenErr: make(chan error, 1),
|
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
|
chanFunder: fundingAssembler,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -22,6 +21,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/shachain"
|
"github.com/lightningnetwork/lnd/shachain"
|
||||||
@ -33,20 +33,6 @@ const (
|
|||||||
msgBufferSize = 100
|
msgBufferSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInsufficientFunds is a type matching the error interface which is
|
|
||||||
// returned when coin selection for a new funding transaction fails to due
|
|
||||||
// having an insufficient amount of confirmed funds.
|
|
||||||
type ErrInsufficientFunds struct {
|
|
||||||
amountAvailable btcutil.Amount
|
|
||||||
amountSelected btcutil.Amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrInsufficientFunds) Error() string {
|
|
||||||
return fmt.Sprintf("not enough witness outputs to create funding transaction,"+
|
|
||||||
" need %v only have %v available", e.amountAvailable,
|
|
||||||
e.amountSelected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
// InitFundingReserveMsg is the first message sent to initiate the workflow
|
||||||
// required to open a payment channel with a remote peer. The initial required
|
// required to open a payment channel with a remote peer. The initial required
|
||||||
// parameters are configurable across channels. These parameters are to be
|
// parameters are configurable across channels. These parameters are to be
|
||||||
@ -62,6 +48,10 @@ type InitFundingReserveMsg struct {
|
|||||||
// target channel.
|
// target channel.
|
||||||
ChainHash *chainhash.Hash
|
ChainHash *chainhash.Hash
|
||||||
|
|
||||||
|
// PendingChanID is the pending channel ID for this funding flow as
|
||||||
|
// used in the wire protocol.
|
||||||
|
PendingChanID [32]byte
|
||||||
|
|
||||||
// NodeID is the ID of the remote node we would like to open a channel
|
// NodeID is the ID of the remote node we would like to open a channel
|
||||||
// with.
|
// with.
|
||||||
NodeID *btcec.PublicKey
|
NodeID *btcec.PublicKey
|
||||||
@ -112,6 +102,12 @@ type InitFundingReserveMsg struct {
|
|||||||
// commitment format or not.
|
// commitment format or not.
|
||||||
Tweakless bool
|
Tweakless bool
|
||||||
|
|
||||||
|
// ChanFunder is an optional channel funder that allows the caller to
|
||||||
|
// control exactly how the channel funding is carried out. If not
|
||||||
|
// specified, then the default chanfunding.WalletAssembler will be
|
||||||
|
// used.
|
||||||
|
ChanFunder chanfunding.Assembler
|
||||||
|
|
||||||
// err is a channel in which all errors will be sent across. Will be
|
// err is a channel in which all errors will be sent across. Will be
|
||||||
// nil if this initial set is successful.
|
// nil if this initial set is successful.
|
||||||
//
|
//
|
||||||
@ -281,6 +277,11 @@ type LightningWallet struct {
|
|||||||
// the currently locked outpoints.
|
// the currently locked outpoints.
|
||||||
lockedOutPoints map[wire.OutPoint]struct{}
|
lockedOutPoints map[wire.OutPoint]struct{}
|
||||||
|
|
||||||
|
// fundingIntents houses all the "interception" registered by a caller
|
||||||
|
// using the RegisterFundingIntent method.
|
||||||
|
intentMtx sync.RWMutex
|
||||||
|
fundingIntents map[[32]byte]chanfunding.Intent
|
||||||
|
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@ -301,6 +302,7 @@ func NewLightningWallet(Cfg Config) (*LightningWallet, error) {
|
|||||||
nextFundingID: 0,
|
nextFundingID: 0,
|
||||||
fundingLimbo: make(map[uint64]*ChannelReservation),
|
fundingLimbo: make(map[uint64]*ChannelReservation),
|
||||||
lockedOutPoints: make(map[wire.OutPoint]struct{}),
|
lockedOutPoints: make(map[wire.OutPoint]struct{}),
|
||||||
|
fundingIntents: make(map[[32]byte]chanfunding.Intent),
|
||||||
quit: make(chan struct{}),
|
quit: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -439,6 +441,21 @@ func (l *LightningWallet) InitChannelReservation(
|
|||||||
return <-req.resp, <-req.err
|
return <-req.resp, <-req.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterFundingIntent allows a caller to signal to the wallet that if a
|
||||||
|
// pending channel ID of expectedID is found, then it can skip constructing a
|
||||||
|
// new chanfunding.Assembler, and instead use the specified chanfunding.Intent.
|
||||||
|
// As an example, this lets some of the parameters for funding transaction to
|
||||||
|
// be negotiated outside the regular funding protocol.
|
||||||
|
func (l *LightningWallet) RegisterFundingIntent(expectedID [32]byte,
|
||||||
|
shimIntent chanfunding.Intent) error {
|
||||||
|
|
||||||
|
l.intentMtx.Lock()
|
||||||
|
l.fundingIntents[expectedID] = shimIntent
|
||||||
|
l.intentMtx.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleFundingReserveRequest processes a message intending to create, and
|
// handleFundingReserveRequest processes a message intending to create, and
|
||||||
// validate a funding reservation request.
|
// validate a funding reservation request.
|
||||||
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
|
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
|
||||||
@ -461,24 +478,55 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no chanFunder was provided, then we'll assume the default
|
||||||
|
// assembler, which is backed by the wallet's internal coin selection.
|
||||||
|
if req.ChanFunder == nil {
|
||||||
|
cfg := chanfunding.WalletConfig{
|
||||||
|
CoinSource: &CoinSource{l},
|
||||||
|
CoinSelectLocker: l,
|
||||||
|
CoinLocker: l,
|
||||||
|
Signer: l.Cfg.Signer,
|
||||||
|
DustLimit: DefaultDustLimit(),
|
||||||
|
}
|
||||||
|
req.ChanFunder = chanfunding.NewWalletAssembler(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
localFundingAmt := req.LocalFundingAmt
|
localFundingAmt := req.LocalFundingAmt
|
||||||
|
remoteFundingAmt := req.RemoteFundingAmt
|
||||||
|
|
||||||
var (
|
var (
|
||||||
selected *coinSelection
|
fundingIntent chanfunding.Intent
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we're on the receiving end of a single funder channel then we
|
// If we've just received an inbound funding request that we have a
|
||||||
// don't need to perform any coin selection, and the remote contributes
|
// registered shim intent to, then we'll obtain the backing intent now.
|
||||||
// all funds. Otherwise, attempt to obtain enough coins to meet the
|
// In this case, we're doing a special funding workflow that allows
|
||||||
// required funding amount.
|
// more advanced constructions such as channel factories to be
|
||||||
if req.LocalFundingAmt != 0 {
|
// instantiated.
|
||||||
|
l.intentMtx.Lock()
|
||||||
|
fundingIntent, ok := l.fundingIntents[req.PendingChanID]
|
||||||
|
l.intentMtx.Unlock()
|
||||||
|
|
||||||
|
// Otherwise, this is a normal funding flow, so we'll use the chan
|
||||||
|
// funder in the attached request to provision the inputs/outputs
|
||||||
|
// that'll ultimately be used to construct the funding transaction.
|
||||||
|
if !ok {
|
||||||
// Coin selection is done on the basis of sat/kw, so we'll use
|
// Coin selection is done on the basis of sat/kw, so we'll use
|
||||||
// the fee rate passed in to perform coin selection.
|
// the fee rate passed in to perform coin selection.
|
||||||
var err error
|
var err error
|
||||||
selected, err = l.selectCoinsAndChange(
|
fundingReq := &chanfunding.Request{
|
||||||
req.FundingFeePerKw, req.LocalFundingAmt, req.MinConfs,
|
RemoteAmt: req.RemoteFundingAmt,
|
||||||
req.SubtractFees,
|
LocalAmt: req.LocalFundingAmt,
|
||||||
|
MinConfs: req.MinConfs,
|
||||||
|
SubtractFees: req.SubtractFees,
|
||||||
|
FeeRate: req.FundingFeePerKw,
|
||||||
|
ChangeAddr: func() (btcutil.Address, error) {
|
||||||
|
return l.NewAddress(WitnessPubKey, true)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fundingIntent, err = req.ChanFunder.ProvisionChannel(
|
||||||
|
fundingReq,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.err <- err
|
req.err <- err
|
||||||
@ -486,31 +534,51 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
localFundingAmt = selected.fundingAmt
|
localFundingAmt = fundingIntent.LocalFundingAmt()
|
||||||
|
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The total channel capacity will be the size of the funding output we
|
// The total channel capacity will be the size of the funding output we
|
||||||
// created plus the remote contribution.
|
// created plus the remote contribution.
|
||||||
capacity := localFundingAmt + req.RemoteFundingAmt
|
capacity := localFundingAmt + remoteFundingAmt
|
||||||
|
|
||||||
id := atomic.AddUint64(&l.nextFundingID, 1)
|
id := atomic.AddUint64(&l.nextFundingID, 1)
|
||||||
reservation, err := NewChannelReservation(
|
reservation, err := NewChannelReservation(
|
||||||
capacity, localFundingAmt, req.CommitFeePerKw, l, id,
|
capacity, localFundingAmt, req.CommitFeePerKw, l, id,
|
||||||
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
|
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
|
||||||
req.Tweakless,
|
req.Tweakless, req.ChanFunder, req.PendingChanID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
selected.unlockCoins()
|
if fundingIntent != nil {
|
||||||
|
fundingIntent.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
req.err <- err
|
req.err <- err
|
||||||
req.resp <- nil
|
req.resp <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keyRing keychain.KeyRing = l.SecretKeyRing
|
||||||
|
|
||||||
|
// If this is a shim intent, then it may be attempting to use an
|
||||||
|
// existing set of keys for the funding workflow. In this case, we'll
|
||||||
|
// make a simple wrapper keychain.KeyRing that will proxy certain
|
||||||
|
// derivation calls to future callers.
|
||||||
|
if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok {
|
||||||
|
keyRing = &shimKeyRing{
|
||||||
|
KeyRing: keyRing,
|
||||||
|
ShimIntent: shimIntent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = l.initOurContribution(
|
err = l.initOurContribution(
|
||||||
reservation, selected, req.NodeAddr, req.NodeID,
|
reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
selected.unlockCoins()
|
if fundingIntent != nil {
|
||||||
|
fundingIntent.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
req.err <- err
|
req.err <- err
|
||||||
req.resp <- nil
|
req.resp <- nil
|
||||||
return
|
return
|
||||||
@ -533,52 +601,60 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
|
|||||||
// and change reserved for the channel, and derives the keys to use for this
|
// and change reserved for the channel, and derives the keys to use for this
|
||||||
// channel.
|
// channel.
|
||||||
func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
|
func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
|
||||||
selected *coinSelection, nodeAddr net.Addr, nodeID *btcec.PublicKey) error {
|
fundingIntent chanfunding.Intent, nodeAddr net.Addr,
|
||||||
|
nodeID *btcec.PublicKey, keyRing keychain.KeyRing) error {
|
||||||
|
|
||||||
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
// Grab the mutex on the ChannelReservation to ensure thread-safety
|
||||||
reservation.Lock()
|
reservation.Lock()
|
||||||
defer reservation.Unlock()
|
defer reservation.Unlock()
|
||||||
|
|
||||||
if selected != nil {
|
// At this point, if we have a funding intent, we'll use it to populate
|
||||||
reservation.ourContribution.Inputs = selected.coins
|
// the existing reservation state entries for our coin selection.
|
||||||
reservation.ourContribution.ChangeOutputs = selected.change
|
if fundingIntent != nil {
|
||||||
|
if intent, ok := fundingIntent.(*chanfunding.FullIntent); ok {
|
||||||
|
for _, coin := range intent.InputCoins {
|
||||||
|
reservation.ourContribution.Inputs = append(
|
||||||
|
reservation.ourContribution.Inputs,
|
||||||
|
&wire.TxIn{
|
||||||
|
PreviousOutPoint: coin.OutPoint,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
reservation.ourContribution.ChangeOutputs = intent.ChangeOutputs
|
||||||
|
}
|
||||||
|
|
||||||
|
reservation.fundingIntent = fundingIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
reservation.nodeAddr = nodeAddr
|
reservation.nodeAddr = nodeAddr
|
||||||
reservation.partialState.IdentityPub = nodeID
|
reservation.partialState.IdentityPub = nodeID
|
||||||
|
|
||||||
// Next, we'll grab a series of keys from the wallet which will be used
|
|
||||||
// for the duration of the channel. The keys include: our multi-sig
|
|
||||||
// key, the base revocation key, the base htlc key,the base payment
|
|
||||||
// key, and the delayed payment key.
|
|
||||||
//
|
|
||||||
// TODO(roasbeef): "salt" each key as well?
|
|
||||||
var err error
|
var err error
|
||||||
reservation.ourContribution.MultiSigKey, err = l.DeriveNextKey(
|
reservation.ourContribution.MultiSigKey, err = keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyMultiSig,
|
keychain.KeyFamilyMultiSig,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reservation.ourContribution.RevocationBasePoint, err = l.DeriveNextKey(
|
reservation.ourContribution.RevocationBasePoint, err = keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyRevocationBase,
|
keychain.KeyFamilyRevocationBase,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reservation.ourContribution.HtlcBasePoint, err = l.DeriveNextKey(
|
reservation.ourContribution.HtlcBasePoint, err = keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyHtlcBase,
|
keychain.KeyFamilyHtlcBase,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reservation.ourContribution.PaymentBasePoint, err = l.DeriveNextKey(
|
reservation.ourContribution.PaymentBasePoint, err = keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyPaymentBase,
|
keychain.KeyFamilyPaymentBase,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reservation.ourContribution.DelayBasePoint, err = l.DeriveNextKey(
|
reservation.ourContribution.DelayBasePoint, err = keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyDelayBase,
|
keychain.KeyFamilyDelayBase,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -587,7 +663,7 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
|
|||||||
|
|
||||||
// With the above keys created, we'll also need to initialization our
|
// With the above keys created, we'll also need to initialization our
|
||||||
// initial revocation tree state.
|
// initial revocation tree state.
|
||||||
nextRevocationKeyDesc, err := l.DeriveNextKey(
|
nextRevocationKeyDesc, err := keyRing.DeriveNextKey(
|
||||||
keychain.KeyFamilyRevocationRoot,
|
keychain.KeyFamilyRevocationRoot,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -653,6 +729,16 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
|
|||||||
|
|
||||||
delete(l.fundingLimbo, req.pendingFundingID)
|
delete(l.fundingLimbo, req.pendingFundingID)
|
||||||
|
|
||||||
|
pid := pendingReservation.pendingChanID
|
||||||
|
|
||||||
|
l.intentMtx.Lock()
|
||||||
|
if intent, ok := l.fundingIntents[pid]; ok {
|
||||||
|
intent.Cancel()
|
||||||
|
|
||||||
|
delete(l.fundingIntents, pendingReservation.pendingChanID)
|
||||||
|
}
|
||||||
|
l.intentMtx.Unlock()
|
||||||
|
|
||||||
req.err <- nil
|
req.err <- nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -722,100 +808,82 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
|||||||
pendingReservation.Lock()
|
pendingReservation.Lock()
|
||||||
defer pendingReservation.Unlock()
|
defer pendingReservation.Unlock()
|
||||||
|
|
||||||
// Create a blank, fresh transaction. Soon to be a complete funding
|
|
||||||
// transaction which will allow opening a lightning channel.
|
|
||||||
pendingReservation.fundingTx = wire.NewMsgTx(1)
|
|
||||||
fundingTx := pendingReservation.fundingTx
|
|
||||||
|
|
||||||
// Some temporary variables to cut down on the resolution verbosity.
|
// Some temporary variables to cut down on the resolution verbosity.
|
||||||
pendingReservation.theirContribution = req.contribution
|
pendingReservation.theirContribution = req.contribution
|
||||||
theirContribution := req.contribution
|
theirContribution := req.contribution
|
||||||
ourContribution := pendingReservation.ourContribution
|
ourContribution := pendingReservation.ourContribution
|
||||||
|
|
||||||
// Add all multi-party inputs and outputs to the transaction.
|
var (
|
||||||
for _, ourInput := range ourContribution.Inputs {
|
chanPoint *wire.OutPoint
|
||||||
fundingTx.AddTxIn(ourInput)
|
err error
|
||||||
}
|
|
||||||
for _, theirInput := range theirContribution.Inputs {
|
|
||||||
fundingTx.AddTxIn(theirInput)
|
|
||||||
}
|
|
||||||
for _, ourChangeOutput := range ourContribution.ChangeOutputs {
|
|
||||||
fundingTx.AddTxOut(ourChangeOutput)
|
|
||||||
}
|
|
||||||
for _, theirChangeOutput := range theirContribution.ChangeOutputs {
|
|
||||||
fundingTx.AddTxOut(theirChangeOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
ourKey := pendingReservation.ourContribution.MultiSigKey
|
|
||||||
theirKey := theirContribution.MultiSigKey
|
|
||||||
|
|
||||||
// Finally, add the 2-of-2 multi-sig output which will set up the lightning
|
|
||||||
// channel.
|
|
||||||
channelCapacity := int64(pendingReservation.partialState.Capacity)
|
|
||||||
witnessScript, multiSigOut, err := input.GenFundingPkScript(
|
|
||||||
ourKey.PubKey.SerializeCompressed(),
|
|
||||||
theirKey.PubKey.SerializeCompressed(), channelCapacity,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// At this point, we can now construct our channel point. Depending on
|
||||||
|
// which type of intent we obtained from our chanfunding.Assembler,
|
||||||
|
// we'll carry out a distinct set of steps.
|
||||||
|
switch fundingIntent := pendingReservation.fundingIntent.(type) {
|
||||||
|
case *chanfunding.ShimIntent:
|
||||||
|
chanPoint, err = fundingIntent.ChanPoint()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.err <- err
|
req.err <- fmt.Errorf("unable to obtain chan point: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the transaction. Since both side agree to a canonical ordering,
|
pendingReservation.partialState.FundingOutpoint = *chanPoint
|
||||||
// by sorting we no longer need to send the entire transaction. Only
|
|
||||||
// signatures will be exchanged.
|
|
||||||
fundingTx.AddTxOut(multiSigOut)
|
|
||||||
txsort.InPlaceSort(pendingReservation.fundingTx)
|
|
||||||
|
|
||||||
// Next, sign all inputs that are ours, collecting the signatures in
|
case *chanfunding.FullIntent:
|
||||||
// order of the inputs.
|
// Now that we know their public key, we can bind theirs as
|
||||||
pendingReservation.ourFundingInputScripts = make([]*input.Script, 0,
|
// well as ours to the funding intent.
|
||||||
len(ourContribution.Inputs))
|
fundingIntent.BindKeys(
|
||||||
signDesc := input.SignDescriptor{
|
&pendingReservation.ourContribution.MultiSigKey,
|
||||||
HashType: txscript.SigHashAll,
|
theirContribution.MultiSigKey.PubKey,
|
||||||
SigHashes: txscript.NewTxSigHashes(fundingTx),
|
)
|
||||||
|
|
||||||
|
// With our keys bound, we can now construct+sign the final
|
||||||
|
// funding transaction and also obtain the chanPoint that
|
||||||
|
// creates the channel.
|
||||||
|
fundingTx, err := fundingIntent.CompileFundingTx(
|
||||||
|
theirContribution.Inputs,
|
||||||
|
theirContribution.ChangeOutputs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
req.err <- fmt.Errorf("unable to construct funding "+
|
||||||
|
"tx: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
for i, txIn := range fundingTx.TxIn {
|
chanPoint, err = fundingIntent.ChanPoint()
|
||||||
info, err := l.FetchInputInfo(&txIn.PreviousOutPoint)
|
if err != nil {
|
||||||
if err == ErrNotMine {
|
req.err <- fmt.Errorf("unable to obtain chan "+
|
||||||
|
"point: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll populate the relevant information in our
|
||||||
|
// pendingReservation so the rest of the funding flow can
|
||||||
|
// continue as normal.
|
||||||
|
pendingReservation.fundingTx = fundingTx
|
||||||
|
pendingReservation.partialState.FundingOutpoint = *chanPoint
|
||||||
|
pendingReservation.ourFundingInputScripts = make(
|
||||||
|
[]*input.Script, 0, len(ourContribution.Inputs),
|
||||||
|
)
|
||||||
|
for _, txIn := range fundingTx.TxIn {
|
||||||
|
_, err := l.FetchInputInfo(&txIn.PreviousOutPoint)
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
} else if err != nil {
|
|
||||||
req.err <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signDesc.Output = &wire.TxOut{
|
|
||||||
PkScript: info.PkScript,
|
|
||||||
Value: int64(info.Value),
|
|
||||||
}
|
|
||||||
signDesc.InputIndex = i
|
|
||||||
|
|
||||||
inputScript, err := l.Cfg.Signer.ComputeInputScript(
|
|
||||||
fundingTx, &signDesc,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
req.err <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
txIn.SignatureScript = inputScript.SigScript
|
|
||||||
txIn.Witness = inputScript.Witness
|
|
||||||
pendingReservation.ourFundingInputScripts = append(
|
pendingReservation.ourFundingInputScripts = append(
|
||||||
pendingReservation.ourFundingInputScripts,
|
pendingReservation.ourFundingInputScripts,
|
||||||
inputScript,
|
&input.Script{
|
||||||
|
Witness: txIn.Witness,
|
||||||
|
SigScript: txIn.SignatureScript,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locate the index of the multi-sig outpoint in order to record it
|
walletLog.Debugf("Funding tx for ChannelPoint(%v) "+
|
||||||
// since the outputs are canonically sorted. If this is a single funder
|
"generated: %v", chanPoint, spew.Sdump(fundingTx))
|
||||||
// workflow, then we'll also need to send this to the remote node.
|
}
|
||||||
fundingTxID := fundingTx.TxHash()
|
|
||||||
_, multiSigIndex := input.FindScriptOutputIndex(fundingTx, multiSigOut.PkScript)
|
|
||||||
fundingOutpoint := wire.NewOutPoint(&fundingTxID, multiSigIndex)
|
|
||||||
pendingReservation.partialState.FundingOutpoint = *fundingOutpoint
|
|
||||||
|
|
||||||
walletLog.Debugf("Funding tx for ChannelPoint(%v) generated: %v",
|
|
||||||
fundingOutpoint, spew.Sdump(fundingTx))
|
|
||||||
|
|
||||||
// Initialize an empty sha-chain for them, tracking the current pending
|
// Initialize an empty sha-chain for them, tracking the current pending
|
||||||
// revocation hash (we don't yet know the preimage so we can't add it
|
// revocation hash (we don't yet know the preimage so we can't add it
|
||||||
@ -832,10 +900,7 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
|||||||
// Create the txin to our commitment transaction; required to construct
|
// Create the txin to our commitment transaction; required to construct
|
||||||
// the commitment transactions.
|
// the commitment transactions.
|
||||||
fundingTxIn := wire.TxIn{
|
fundingTxIn := wire.TxIn{
|
||||||
PreviousOutPoint: wire.OutPoint{
|
PreviousOutPoint: *chanPoint,
|
||||||
Hash: fundingTxID,
|
|
||||||
Index: multiSigIndex,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the funding tx complete, create both commitment transactions.
|
// With the funding tx complete, create both commitment transactions.
|
||||||
@ -892,21 +957,32 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
|
|||||||
txsort.InPlaceSort(theirCommitTx)
|
txsort.InPlaceSort(theirCommitTx)
|
||||||
|
|
||||||
walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v",
|
walletLog.Debugf("Local commit tx for ChannelPoint(%v): %v",
|
||||||
fundingOutpoint, spew.Sdump(ourCommitTx))
|
chanPoint, spew.Sdump(ourCommitTx))
|
||||||
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v",
|
walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v",
|
||||||
fundingOutpoint, spew.Sdump(theirCommitTx))
|
chanPoint, spew.Sdump(theirCommitTx))
|
||||||
|
|
||||||
// Record newly available information within the open channel state.
|
// Record newly available information within the open channel state.
|
||||||
chanState.FundingOutpoint = *fundingOutpoint
|
chanState.FundingOutpoint = *chanPoint
|
||||||
chanState.LocalCommitment.CommitTx = ourCommitTx
|
chanState.LocalCommitment.CommitTx = ourCommitTx
|
||||||
chanState.RemoteCommitment.CommitTx = theirCommitTx
|
chanState.RemoteCommitment.CommitTx = theirCommitTx
|
||||||
|
|
||||||
|
// Next, we'll obtain the funding witness script, and the funding
|
||||||
|
// output itself so we can generate a valid signature for the remote
|
||||||
|
// party.
|
||||||
|
fundingIntent := pendingReservation.fundingIntent
|
||||||
|
fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput()
|
||||||
|
if err != nil {
|
||||||
|
req.err <- fmt.Errorf("unable to obtain funding output")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a signature for their version of the initial commitment
|
// Generate a signature for their version of the initial commitment
|
||||||
// transaction.
|
// transaction.
|
||||||
signDesc = input.SignDescriptor{
|
ourKey := ourContribution.MultiSigKey
|
||||||
WitnessScript: witnessScript,
|
signDesc := input.SignDescriptor{
|
||||||
|
WitnessScript: fundingWitnessScript,
|
||||||
KeyDesc: ourKey,
|
KeyDesc: ourKey,
|
||||||
Output: multiSigOut,
|
Output: fundingOutput,
|
||||||
HashType: txscript.SigHashAll,
|
HashType: txscript.SigHashAll,
|
||||||
SigHashes: txscript.NewTxSigHashes(theirCommitTx),
|
SigHashes: txscript.NewTxSigHashes(theirCommitTx),
|
||||||
InputIndex: 0,
|
InputIndex: 0,
|
||||||
@ -962,12 +1038,64 @@ func (l *LightningWallet) handleSingleContribution(req *addSingleContributionMsg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// openChanDetails contains a "finalized" channel which can be considered
|
// verifyFundingInputs attempts to verify all remote inputs to the funding
|
||||||
// "open" according to the requested confirmation depth at reservation
|
// transaction.
|
||||||
// initialization. Additionally, the struct contains additional details
|
func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx,
|
||||||
// pertaining to the exact location in the main chain in-which the transaction
|
remoteInputScripts []*input.Script) error {
|
||||||
// was confirmed.
|
|
||||||
type openChanDetails struct {
|
sigIndex := 0
|
||||||
|
fundingHashCache := txscript.NewTxSigHashes(fundingTx)
|
||||||
|
inputScripts := remoteInputScripts
|
||||||
|
for i, txin := range fundingTx.TxIn {
|
||||||
|
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
|
||||||
|
// Attach the input scripts so we can verify it below.
|
||||||
|
txin.Witness = inputScripts[sigIndex].Witness
|
||||||
|
txin.SignatureScript = inputScripts[sigIndex].SigScript
|
||||||
|
|
||||||
|
// Fetch the alleged previous output along with the
|
||||||
|
// pkscript referenced by this input.
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): when dual funder pass actual
|
||||||
|
// height-hint
|
||||||
|
//
|
||||||
|
// TODO(roasbeef): this fails for neutrino always as it
|
||||||
|
// treats the height hint as an exact birthday of the
|
||||||
|
// utxo rather than a lower bound
|
||||||
|
pkScript, err := txscript.ComputePkScript(
|
||||||
|
txin.SignatureScript, txin.Witness,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create script: %v", err)
|
||||||
|
}
|
||||||
|
output, err := l.Cfg.ChainIO.GetUtxo(
|
||||||
|
&txin.PreviousOutPoint,
|
||||||
|
pkScript.Script(), 0, l.quit,
|
||||||
|
)
|
||||||
|
if output == nil {
|
||||||
|
return fmt.Errorf("input to funding tx does "+
|
||||||
|
"not exist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the witness+sigScript combo is valid.
|
||||||
|
vm, err := txscript.NewEngine(
|
||||||
|
output.PkScript, fundingTx, i,
|
||||||
|
txscript.StandardVerifyFlags, nil,
|
||||||
|
fundingHashCache, output.Value,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create script "+
|
||||||
|
"engine: %s", err)
|
||||||
|
}
|
||||||
|
if err = vm.Execute(); err != nil {
|
||||||
|
return fmt.Errorf("cannot validate "+
|
||||||
|
"transaction: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleFundingCounterPartySigs is the final step in the channel reservation
|
// handleFundingCounterPartySigs is the final step in the channel reservation
|
||||||
@ -994,60 +1122,18 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
|
|||||||
// signatures to their inputs.
|
// signatures to their inputs.
|
||||||
res.theirFundingInputScripts = msg.theirFundingInputScripts
|
res.theirFundingInputScripts = msg.theirFundingInputScripts
|
||||||
inputScripts := msg.theirFundingInputScripts
|
inputScripts := msg.theirFundingInputScripts
|
||||||
|
|
||||||
|
// Only if we have the final funding transaction do we need to verify
|
||||||
|
// the final set of inputs. Otherwise, it may be the case that the
|
||||||
|
// channel was funded via an external wallet.
|
||||||
fundingTx := res.fundingTx
|
fundingTx := res.fundingTx
|
||||||
sigIndex := 0
|
if res.partialState.ChanType.HasFundingTx() {
|
||||||
fundingHashCache := txscript.NewTxSigHashes(fundingTx)
|
err := l.verifyFundingInputs(fundingTx, inputScripts)
|
||||||
for i, txin := range fundingTx.TxIn {
|
|
||||||
if len(inputScripts) != 0 && len(txin.Witness) == 0 {
|
|
||||||
// Attach the input scripts so we can verify it below.
|
|
||||||
txin.Witness = inputScripts[sigIndex].Witness
|
|
||||||
txin.SignatureScript = inputScripts[sigIndex].SigScript
|
|
||||||
|
|
||||||
// Fetch the alleged previous output along with the
|
|
||||||
// pkscript referenced by this input.
|
|
||||||
//
|
|
||||||
// TODO(roasbeef): when dual funder pass actual
|
|
||||||
// height-hint
|
|
||||||
pkScript, err := input.WitnessScriptHash(
|
|
||||||
txin.Witness[len(txin.Witness)-1],
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.err <- fmt.Errorf("cannot create script: "+
|
msg.err <- err
|
||||||
"%v", err)
|
|
||||||
msg.completeChan <- nil
|
msg.completeChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := l.Cfg.ChainIO.GetUtxo(
|
|
||||||
&txin.PreviousOutPoint,
|
|
||||||
pkScript, 0, l.quit,
|
|
||||||
)
|
|
||||||
if output == nil {
|
|
||||||
msg.err <- fmt.Errorf("input to funding tx "+
|
|
||||||
"does not exist: %v", err)
|
|
||||||
msg.completeChan <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the witness+sigScript combo is valid.
|
|
||||||
vm, err := txscript.NewEngine(output.PkScript,
|
|
||||||
fundingTx, i, txscript.StandardVerifyFlags, nil,
|
|
||||||
fundingHashCache, output.Value)
|
|
||||||
if err != nil {
|
|
||||||
msg.err <- fmt.Errorf("cannot create script "+
|
|
||||||
"engine: %s", err)
|
|
||||||
msg.completeChan <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = vm.Execute(); err != nil {
|
|
||||||
msg.err <- fmt.Errorf("cannot validate "+
|
|
||||||
"transaction: %s", err)
|
|
||||||
msg.completeChan <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sigIndex++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, we can also record and verify their signature for our
|
// At this point, we can also record and verify their signature for our
|
||||||
@ -1075,8 +1161,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
|
|||||||
// is complete, allowing us to spend from the funding transaction.
|
// is complete, allowing us to spend from the funding transaction.
|
||||||
channelValue := int64(res.partialState.Capacity)
|
channelValue := int64(res.partialState.Capacity)
|
||||||
hashCache := txscript.NewTxSigHashes(commitTx)
|
hashCache := txscript.NewTxSigHashes(commitTx)
|
||||||
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache,
|
sigHash, err := txscript.CalcWitnessSigHash(
|
||||||
txscript.SigHashAll, commitTx, 0, channelValue)
|
witnessScript, hashCache, txscript.SigHashAll, commitTx,
|
||||||
|
0, channelValue,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg.err <- err
|
msg.err <- err
|
||||||
msg.completeChan <- nil
|
msg.completeChan <- nil
|
||||||
@ -1103,6 +1191,10 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
|
|||||||
delete(l.fundingLimbo, res.reservationID)
|
delete(l.fundingLimbo, res.reservationID)
|
||||||
l.limboMtx.Unlock()
|
l.limboMtx.Unlock()
|
||||||
|
|
||||||
|
l.intentMtx.Lock()
|
||||||
|
delete(l.fundingIntents, res.pendingChanID)
|
||||||
|
l.intentMtx.Unlock()
|
||||||
|
|
||||||
// As we're about to broadcast the funding transaction, we'll take note
|
// As we're about to broadcast the funding transaction, we'll take note
|
||||||
// of the current height for record keeping purposes.
|
// of the current height for record keeping purposes.
|
||||||
//
|
//
|
||||||
@ -1223,8 +1315,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sigHash, err := txscript.CalcWitnessSigHash(witnessScript, hashCache,
|
sigHash, err := txscript.CalcWitnessSigHash(
|
||||||
txscript.SigHashAll, ourCommitTx, 0, channelValue)
|
witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0,
|
||||||
|
channelValue,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.err <- err
|
req.err <- err
|
||||||
req.completeChan <- nil
|
req.completeChan <- nil
|
||||||
@ -1238,7 +1332,8 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
|||||||
req.err <- err
|
req.err <- err
|
||||||
req.completeChan <- nil
|
req.completeChan <- nil
|
||||||
return
|
return
|
||||||
} else if !sig.Verify(sigHash, theirKey.PubKey) {
|
}
|
||||||
|
if !sig.Verify(sigHash, theirKey.PubKey) {
|
||||||
req.err <- fmt.Errorf("counterparty's commitment signature " +
|
req.err <- fmt.Errorf("counterparty's commitment signature " +
|
||||||
"is invalid")
|
"is invalid")
|
||||||
req.completeChan <- nil
|
req.completeChan <- nil
|
||||||
@ -1298,6 +1393,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
|
|||||||
l.limboMtx.Lock()
|
l.limboMtx.Lock()
|
||||||
delete(l.fundingLimbo, req.pendingFundingID)
|
delete(l.fundingLimbo, req.pendingFundingID)
|
||||||
l.limboMtx.Unlock()
|
l.limboMtx.Unlock()
|
||||||
|
|
||||||
|
l.intentMtx.Lock()
|
||||||
|
delete(l.fundingIntents, pendingReservation.pendingChanID)
|
||||||
|
l.intentMtx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithCoinSelectLock will execute the passed function closure in a
|
// WithCoinSelectLock will execute the passed function closure in a
|
||||||
@ -1311,127 +1410,6 @@ func (l *LightningWallet) WithCoinSelectLock(f func() error) error {
|
|||||||
return f()
|
return f()
|
||||||
}
|
}
|
||||||
|
|
||||||
// coinSelection holds the result from selectCoinsAndChange.
|
|
||||||
type coinSelection struct {
|
|
||||||
coins []*wire.TxIn
|
|
||||||
change []*wire.TxOut
|
|
||||||
fundingAmt btcutil.Amount
|
|
||||||
unlockCoins func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectCoinsAndChange performs coin selection in order to obtain witness
|
|
||||||
// outputs which sum to at least 'amt' amount of satoshis. If necessary,
|
|
||||||
// a change address will also be generated. If coin selection is
|
|
||||||
// successful/possible, then the selected coins and change outputs are
|
|
||||||
// returned, and the value of the resulting funding output. This method locks
|
|
||||||
// the selected outputs, and a function closure to unlock them in case of an
|
|
||||||
// error is returned.
|
|
||||||
func (l *LightningWallet) selectCoinsAndChange(feeRate chainfee.SatPerKWeight,
|
|
||||||
amt btcutil.Amount, minConfs int32, subtractFees bool) (
|
|
||||||
*coinSelection, error) {
|
|
||||||
|
|
||||||
// We hold the coin select mutex while querying for outputs, and
|
|
||||||
// performing coin selection in order to avoid inadvertent double
|
|
||||||
// spends across funding transactions.
|
|
||||||
l.coinSelectMtx.Lock()
|
|
||||||
defer l.coinSelectMtx.Unlock()
|
|
||||||
|
|
||||||
walletLog.Infof("Performing funding tx coin selection using %v "+
|
|
||||||
"sat/kw as fee rate", int64(feeRate))
|
|
||||||
|
|
||||||
// Find all unlocked unspent witness outputs that satisfy the minimum
|
|
||||||
// number of confirmations required.
|
|
||||||
coins, err := l.ListUnspentWitness(minConfs, math.MaxInt32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
selectedCoins []*Utxo
|
|
||||||
fundingAmt btcutil.Amount
|
|
||||||
changeAmt btcutil.Amount
|
|
||||||
)
|
|
||||||
|
|
||||||
// Perform coin selection over our available, unlocked unspent outputs
|
|
||||||
// in order to find enough coins to meet the funding amount
|
|
||||||
// requirements.
|
|
||||||
switch {
|
|
||||||
// In case this request want the fees subtracted from the local amount,
|
|
||||||
// we'll call the specialized method for that. This ensures that we
|
|
||||||
// won't deduct more that the specified balance from our wallet.
|
|
||||||
case subtractFees:
|
|
||||||
dustLimit := l.Cfg.DefaultConstraints.DustLimit
|
|
||||||
selectedCoins, fundingAmt, changeAmt, err = coinSelectSubtractFees(
|
|
||||||
feeRate, amt, dustLimit, coins,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ótherwise do a normal coin selection where we target a given funding
|
|
||||||
// amount.
|
|
||||||
default:
|
|
||||||
fundingAmt = amt
|
|
||||||
selectedCoins, changeAmt, err = coinSelect(feeRate, amt, coins)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record any change output(s) generated as a result of the coin
|
|
||||||
// selection, but only if the addition of the output won't lead to the
|
|
||||||
// creation of dust.
|
|
||||||
var changeOutputs []*wire.TxOut
|
|
||||||
if changeAmt != 0 && changeAmt > DefaultDustLimit() {
|
|
||||||
changeAddr, err := l.NewAddress(WitnessPubKey, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
changeScript, err := txscript.PayToAddrScript(changeAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
changeOutputs = make([]*wire.TxOut, 1)
|
|
||||||
changeOutputs[0] = &wire.TxOut{
|
|
||||||
Value: int64(changeAmt),
|
|
||||||
PkScript: changeScript,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock the selected coins. These coins are now "reserved", this
|
|
||||||
// prevents concurrent funding requests from referring to and this
|
|
||||||
// double-spending the same set of coins.
|
|
||||||
inputs := make([]*wire.TxIn, len(selectedCoins))
|
|
||||||
for i, coin := range selectedCoins {
|
|
||||||
outpoint := &coin.OutPoint
|
|
||||||
l.lockedOutPoints[*outpoint] = struct{}{}
|
|
||||||
l.LockOutpoint(*outpoint)
|
|
||||||
|
|
||||||
// Empty sig script, we'll actually sign if this reservation is
|
|
||||||
// queued up to be completed (the other side accepts).
|
|
||||||
inputs[i] = wire.NewTxIn(outpoint, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
unlock := func() {
|
|
||||||
l.coinSelectMtx.Lock()
|
|
||||||
defer l.coinSelectMtx.Unlock()
|
|
||||||
|
|
||||||
for _, coin := range selectedCoins {
|
|
||||||
outpoint := &coin.OutPoint
|
|
||||||
delete(l.lockedOutPoints, *outpoint)
|
|
||||||
l.UnlockOutpoint(*outpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &coinSelection{
|
|
||||||
coins: inputs,
|
|
||||||
change: changeOutputs,
|
|
||||||
fundingAmt: fundingAmt,
|
|
||||||
unlockCoins: unlock,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveStateHintObfuscator derives the bytes to be used for obfuscating the
|
// DeriveStateHintObfuscator derives the bytes to be used for obfuscating the
|
||||||
// state hints from the root to be used for a new channel. The obfuscator is
|
// state hints from the root to be used for a new channel. The obfuscator is
|
||||||
// generated via the following computation:
|
// generated via the following computation:
|
||||||
@ -1468,179 +1446,6 @@ func initStateHints(commit1, commit2 *wire.MsgTx,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectInputs selects a slice of inputs necessary to meet the specified
|
|
||||||
// selection amount. If input selection is unable to succeed due to insufficient
|
|
||||||
// funds, a non-nil error is returned. Additionally, the total amount of the
|
|
||||||
// selected coins are returned in order for the caller to properly handle
|
|
||||||
// change+fees.
|
|
||||||
func selectInputs(amt btcutil.Amount, coins []*Utxo) (btcutil.Amount, []*Utxo, error) {
|
|
||||||
satSelected := btcutil.Amount(0)
|
|
||||||
for i, coin := range coins {
|
|
||||||
satSelected += coin.Value
|
|
||||||
if satSelected >= amt {
|
|
||||||
return satSelected, coins[:i+1], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, nil, &ErrInsufficientFunds{amt, satSelected}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coinSelect attempts to select a sufficient amount of coins, including a
|
|
||||||
// change output to fund amt satoshis, adhering to the specified fee rate. The
|
|
||||||
// specified fee rate should be expressed in sat/kw for coin selection to
|
|
||||||
// function properly.
|
|
||||||
func coinSelect(feeRate chainfee.SatPerKWeight, amt btcutil.Amount,
|
|
||||||
coins []*Utxo) ([]*Utxo, btcutil.Amount, error) {
|
|
||||||
|
|
||||||
amtNeeded := amt
|
|
||||||
for {
|
|
||||||
// First perform an initial round of coin selection to estimate
|
|
||||||
// the required fee.
|
|
||||||
totalSat, selectedUtxos, err := selectInputs(amtNeeded, coins)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var weightEstimate input.TxWeightEstimator
|
|
||||||
|
|
||||||
for _, utxo := range selectedUtxos {
|
|
||||||
switch utxo.AddressType {
|
|
||||||
case WitnessPubKey:
|
|
||||||
weightEstimate.AddP2WKHInput()
|
|
||||||
case NestedWitnessPubKey:
|
|
||||||
weightEstimate.AddNestedP2WKHInput()
|
|
||||||
default:
|
|
||||||
return nil, 0, fmt.Errorf("unsupported address type: %v",
|
|
||||||
utxo.AddressType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel funding multisig output is P2WSH.
|
|
||||||
weightEstimate.AddP2WSHOutput()
|
|
||||||
|
|
||||||
// Assume that change output is a P2WKH output.
|
|
||||||
//
|
|
||||||
// TODO: Handle wallets that generate non-witness change
|
|
||||||
// addresses.
|
|
||||||
// TODO(halseth): make coinSelect not estimate change output
|
|
||||||
// for dust change.
|
|
||||||
weightEstimate.AddP2WKHOutput()
|
|
||||||
|
|
||||||
// The difference between the selected amount and the amount
|
|
||||||
// requested will be used to pay fees, and generate a change
|
|
||||||
// output with the remaining.
|
|
||||||
overShootAmt := totalSat - amt
|
|
||||||
|
|
||||||
// Based on the estimated size and fee rate, if the excess
|
|
||||||
// amount isn't enough to pay fees, then increase the requested
|
|
||||||
// coin amount by the estimate required fee, performing another
|
|
||||||
// round of coin selection.
|
|
||||||
totalWeight := int64(weightEstimate.Weight())
|
|
||||||
requiredFee := feeRate.FeeForWeight(totalWeight)
|
|
||||||
if overShootAmt < requiredFee {
|
|
||||||
amtNeeded = amt + requiredFee
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the fee is sufficient, then calculate the size of the
|
|
||||||
// change output.
|
|
||||||
changeAmt := overShootAmt - requiredFee
|
|
||||||
|
|
||||||
return selectedUtxos, changeAmt, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coinSelectSubtractFees attempts to select coins such that we'll spend up to
|
|
||||||
// amt in total after fees, adhering to the specified fee rate. The selected
|
|
||||||
// coins, the final output and change values are returned.
|
|
||||||
func coinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
|
|
||||||
dustLimit btcutil.Amount, coins []*Utxo) ([]*Utxo, btcutil.Amount,
|
|
||||||
btcutil.Amount, error) {
|
|
||||||
|
|
||||||
// First perform an initial round of coin selection to estimate
|
|
||||||
// the required fee.
|
|
||||||
totalSat, selectedUtxos, err := selectInputs(amt, coins)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var weightEstimate input.TxWeightEstimator
|
|
||||||
for _, utxo := range selectedUtxos {
|
|
||||||
switch utxo.AddressType {
|
|
||||||
case WitnessPubKey:
|
|
||||||
weightEstimate.AddP2WKHInput()
|
|
||||||
case NestedWitnessPubKey:
|
|
||||||
weightEstimate.AddNestedP2WKHInput()
|
|
||||||
default:
|
|
||||||
return nil, 0, 0, fmt.Errorf("unsupported "+
|
|
||||||
"address type: %v", utxo.AddressType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel funding multisig output is P2WSH.
|
|
||||||
weightEstimate.AddP2WSHOutput()
|
|
||||||
|
|
||||||
// At this point we've got two possibilities, either create a
|
|
||||||
// change output, or not. We'll first try without creating a
|
|
||||||
// change output.
|
|
||||||
//
|
|
||||||
// Estimate the fee required for a transaction without a change
|
|
||||||
// output.
|
|
||||||
totalWeight := int64(weightEstimate.Weight())
|
|
||||||
requiredFee := feeRate.FeeForWeight(totalWeight)
|
|
||||||
|
|
||||||
// For a transaction without a change output, we'll let everything go
|
|
||||||
// to our multi-sig output after subtracting fees.
|
|
||||||
outputAmt := totalSat - requiredFee
|
|
||||||
changeAmt := btcutil.Amount(0)
|
|
||||||
|
|
||||||
// If the the output is too small after subtracting the fee, the coin
|
|
||||||
// selection cannot be performed with an amount this small.
|
|
||||||
if outputAmt <= dustLimit {
|
|
||||||
return nil, 0, 0, fmt.Errorf("output amount(%v) after "+
|
|
||||||
"subtracting fees(%v) below dust limit(%v)", outputAmt,
|
|
||||||
requiredFee, dustLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We were able to create a transaction with no change from the
|
|
||||||
// selected inputs. We'll remember the resulting values for
|
|
||||||
// now, while we try to add a change output. Assume that change output
|
|
||||||
// is a P2WKH output.
|
|
||||||
weightEstimate.AddP2WKHOutput()
|
|
||||||
|
|
||||||
// Now that we have added the change output, redo the fee
|
|
||||||
// estimate.
|
|
||||||
totalWeight = int64(weightEstimate.Weight())
|
|
||||||
requiredFee = feeRate.FeeForWeight(totalWeight)
|
|
||||||
|
|
||||||
// For a transaction with a change output, everything we don't spend
|
|
||||||
// will go to change.
|
|
||||||
newChange := totalSat - amt
|
|
||||||
newOutput := amt - requiredFee
|
|
||||||
|
|
||||||
// If adding a change output leads to both outputs being above
|
|
||||||
// the dust limit, we'll add the change output. Otherwise we'll
|
|
||||||
// go with the no change tx we originally found.
|
|
||||||
if newChange > dustLimit && newOutput > dustLimit {
|
|
||||||
outputAmt = newOutput
|
|
||||||
changeAmt = newChange
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check the resulting output values to make sure we
|
|
||||||
// don't burn a great part to fees.
|
|
||||||
totalOut := outputAmt + changeAmt
|
|
||||||
fee := totalSat - totalOut
|
|
||||||
|
|
||||||
// Fail if more than 20% goes to fees.
|
|
||||||
// TODO(halseth): smarter fee limit. Make configurable or dynamic wrt
|
|
||||||
// total funding size?
|
|
||||||
if fee > totalOut/5 {
|
|
||||||
return nil, 0, 0, fmt.Errorf("fee %v on total output"+
|
|
||||||
"value %v", fee, totalOut)
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedUtxos, outputAmt, changeAmt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
// ValidateChannel will attempt to fully validate a newly mined channel, given
|
||||||
// its funding transaction and existing channel state. If this method returns
|
// its funding transaction and existing channel state. If this method returns
|
||||||
// an error, then the mined channel is invalid, and shouldn't be used.
|
// an error, then the mined channel is invalid, and shouldn't be used.
|
||||||
@ -1695,3 +1500,81 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoinSource is a wrapper around the wallet that implements the
|
||||||
|
// chanfunding.CoinSource interface.
|
||||||
|
type CoinSource struct {
|
||||||
|
wallet *LightningWallet
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCoinSource creates a new instance of the CoinSource wrapper struct.
|
||||||
|
func NewCoinSource(w *LightningWallet) *CoinSource {
|
||||||
|
return &CoinSource{wallet: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCoins returns all UTXOs from the source that have between
|
||||||
|
// minConfs and maxConfs number of confirmations.
|
||||||
|
func (c *CoinSource) ListCoins(minConfs int32,
|
||||||
|
maxConfs int32) ([]chanfunding.Coin, error) {
|
||||||
|
|
||||||
|
utxos, err := c.wallet.ListUnspentWitness(minConfs, maxConfs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var coins []chanfunding.Coin
|
||||||
|
for _, utxo := range utxos {
|
||||||
|
coins = append(coins, chanfunding.Coin{
|
||||||
|
TxOut: wire.TxOut{
|
||||||
|
Value: int64(utxo.Value),
|
||||||
|
PkScript: utxo.PkScript,
|
||||||
|
},
|
||||||
|
OutPoint: utxo.OutPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return coins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CoinFromOutPoint attempts to locate details pertaining to a coin based on
|
||||||
|
// its outpoint. If the coin isn't under the control of the backing CoinSource,
|
||||||
|
// then an error should be returned.
|
||||||
|
func (c *CoinSource) CoinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, error) {
|
||||||
|
inputInfo, err := c.wallet.FetchInputInfo(&op)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chanfunding.Coin{
|
||||||
|
TxOut: wire.TxOut{
|
||||||
|
Value: int64(inputInfo.Value),
|
||||||
|
PkScript: inputInfo.PkScript,
|
||||||
|
},
|
||||||
|
OutPoint: inputInfo.OutPoint,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shimKeyRing is a wrapper struct that's used to provide the proper multi-sig
|
||||||
|
// key for an initiated external funding flow.
|
||||||
|
type shimKeyRing struct {
|
||||||
|
keychain.KeyRing
|
||||||
|
|
||||||
|
*chanfunding.ShimIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveNextKey intercepts the normal DeriveNextKey call to a keychain.KeyRing
|
||||||
|
// instance, and supplies the multi-sig key specified by the ShimIntent. This
|
||||||
|
// allows us to transparently insert new keys into the existing funding flow,
|
||||||
|
// as these keys may not come from the wallet itself.
|
||||||
|
func (s *shimKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) {
|
||||||
|
if keyFam != keychain.KeyFamilyMultiSig {
|
||||||
|
return s.KeyRing.DeriveNextKey(keyFam)
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingKeys, err := s.ShimIntent.MultiSigKeys()
|
||||||
|
if err != nil {
|
||||||
|
return keychain.KeyDescriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *fundingKeys.LocalKey, nil
|
||||||
|
}
|
||||||
|
2
log.go
2
log.go
@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||||
"github.com/lightningnetwork/lnd/monitoring"
|
"github.com/lightningnetwork/lnd/monitoring"
|
||||||
"github.com/lightningnetwork/lnd/netann"
|
"github.com/lightningnetwork/lnd/netann"
|
||||||
"github.com/lightningnetwork/lnd/peernotifier"
|
"github.com/lightningnetwork/lnd/peernotifier"
|
||||||
@ -96,6 +97,7 @@ func init() {
|
|||||||
addSubLogger("PROM", monitoring.UseLogger)
|
addSubLogger("PROM", monitoring.UseLogger)
|
||||||
addSubLogger("WTCL", wtclient.UseLogger)
|
addSubLogger("WTCL", wtclient.UseLogger)
|
||||||
addSubLogger("PRNF", peernotifier.UseLogger)
|
addSubLogger("PRNF", peernotifier.UseLogger)
|
||||||
|
addSubLogger("CHFD", chanfunding.UseLogger)
|
||||||
|
|
||||||
addSubLogger(routerrpc.Subsystem, routerrpc.UseLogger)
|
addSubLogger(routerrpc.Subsystem, routerrpc.UseLogger)
|
||||||
addSubLogger(wtclientrpc.Subsystem, wtclientrpc.UseLogger)
|
addSubLogger(wtclientrpc.Subsystem, wtclientrpc.UseLogger)
|
||||||
|
7
mock.go
7
mock.go
@ -1,6 +1,7 @@
|
|||||||
package lnd
|
package lnd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -20,6 +21,10 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
coinPkScript, _ = hex.DecodeString("001431df1bde03c074d0cf21ea2529427e1499b8f1de")
|
||||||
|
)
|
||||||
|
|
||||||
// The block height returned by the mock BlockChainIO's GetBestBlock.
|
// The block height returned by the mock BlockChainIO's GetBestBlock.
|
||||||
const fundingBroadcastHeight = 123
|
const fundingBroadcastHeight = 123
|
||||||
|
|
||||||
@ -297,7 +302,7 @@ func (m *mockWalletController) ListUnspentWitness(minconfirms,
|
|||||||
utxo := &lnwallet.Utxo{
|
utxo := &lnwallet.Utxo{
|
||||||
AddressType: lnwallet.WitnessPubKey,
|
AddressType: lnwallet.WitnessPubKey,
|
||||||
Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin),
|
Value: btcutil.Amount(10 * btcutil.SatoshiPerBitcoin),
|
||||||
PkScript: make([]byte, 22),
|
PkScript: coinPkScript,
|
||||||
OutPoint: wire.OutPoint{
|
OutPoint: wire.OutPoint{
|
||||||
Hash: chainhash.Hash{},
|
Hash: chainhash.Hash{},
|
||||||
Index: m.index,
|
Index: m.index,
|
||||||
|
Loading…
Reference in New Issue
Block a user