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