lnd.xprv/lnwallet/chanfunding/wallet_assembler.go
Olaoluwa Osuntokun d422ebbc66
lnwallet/chanfunding: introduce new channel funding abstractions
In this commit, we introduce a series of new abstractions for channel
funding. The end goal is to enable uses cases that construct the funding
transaction externally, eventually handing the funding outpoint to lnd.
An example of such a use case includes channel factories and external
channel funding using a hardware wallet.

We also add a new chanfunding.Assembler meant to allow external channel
funding in contexts similar to how channel factories
can be constructed. With this channel funder, we'll only obtain the
channel point and funding output from it, as this alone is enough to
carry out a funding flow as normal.
2019-12-02 17:11:32 -06:00

344 lines
11 KiB
Go

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)