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.
This commit is contained in:
Olaoluwa Osuntokun 2019-10-31 21:32:04 -07:00
parent 4e955dfac6
commit d422ebbc66
No known key found for this signature in database
GPG Key ID: BC13F65E2DC84465
5 changed files with 698 additions and 0 deletions

@ -0,0 +1,137 @@
package chanfunding
import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// CoinSource is an interface that allows a caller to access a source of UTXOs
// to use when attempting to fund a new channel.
type CoinSource interface {
// ListCoins returns all UTXOs from the source that have between
// minConfs and maxConfs number of confirmations.
ListCoins(minConfs, maxConfs int32) ([]Coin, error)
// CoinFromOutPoint attempts to locate details pertaining to a coin
// based on its outpoint. If the coin isn't under the control of the
// backing CoinSource, then an error should be returned.
CoinFromOutPoint(wire.OutPoint) (*Coin, error)
}
// CoinSelectionLocker is an interface that allows the caller to perform an
// operation, which is synchronized with all coin selection attempts. This can
// be used when an operation requires that all coin selection operations cease
// forward progress. Think of this as an exclusive lock on coin selection
// operations.
type CoinSelectionLocker interface {
// WithCoinSelectLock will execute the passed function closure in a
// synchronized manner preventing any coin selection operations from
// proceeding while the closure if executing. This can be seen as the
// ability to execute a function closure under an exclusive coin
// selection lock.
WithCoinSelectLock(func() error) error
}
// OutpointLocker allows a caller to lock/unlock an outpoint. When locked, the
// outpoints shouldn't be used for any sort of channel funding of coin
// selection. Locked outpoints are not expected to be persisted between
// restarts.
type OutpointLocker interface {
// LockOutpoint locks a target outpoint, rendering it unusable for coin
// selection.
LockOutpoint(o wire.OutPoint)
// UnlockOutpoint unlocks a target outpoint, allowing it to be used for
// coin selection once again.
UnlockOutpoint(o wire.OutPoint)
}
// Request is a new request for funding a channel. The items in the struct
// governs how the final channel point will be provisioned by the target
// Assembler.
type Request struct {
// LocalAmt is the amount of coins we're placing into the funding
// output.
LocalAmt btcutil.Amount
// RemoteAmt is the amount of coins the remote party is contributing to
// the funding output.
RemoteAmt btcutil.Amount
// MinConfs controls how many confirmations a coin need to be eligible
// to be used as an input to the funding transaction. If this value is
// set to zero, then zero conf outputs may be spent.
MinConfs int32
// SubtractFees should be set if we intend to spend exactly LocalAmt
// when opening the channel, subtracting the fees from the funding
// output. This can be used for instance to use all our remaining funds
// to open the channel, since it will take fees into
// account.
SubtractFees bool
// FeeRate is the fee rate in sat/kw that the funding transaction
// should carry.
FeeRate chainfee.SatPerKWeight
// ChangeAddr is a closure that will provide the Assembler with a
// change address for the funding transaction if needed.
ChangeAddr func() (btcutil.Address, error)
}
// Intent is returned by an Assembler and represents the base functionality the
// caller needs to proceed with channel funding on a higher level. If the
// Cancel method is called, then all resources assembled to fund the channel
// will be released back to the eligible pool.
type Intent interface {
// FundingOutput returns the witness script, and the output that
// creates the funding output.
FundingOutput() ([]byte, *wire.TxOut, error)
// ChanPoint returns the final outpoint that will create the funding
// output described above.
ChanPoint() (*wire.OutPoint, error)
// RemoteFundingAmt is the amount the remote party put into the
// channel.
RemoteFundingAmt() btcutil.Amount
// LocalFundingAmt is the amount we put into the channel. This may
// differ from the local amount requested, as depending on coin
// selection, we may bleed from of that LocalAmt into fees to minimize
// change.
LocalFundingAmt() btcutil.Amount
// Cancel allows the caller to cancel a funding Intent at any time.
// This will return any resources such as coins back to the eligible
// pool to be used in order channel fundings.
Cancel()
}
// Assembler is an abstract object that is capable of assembling everything
// needed to create a new funding output. As an example, this assembler may be
// our core backing wallet, an interactive PSBT based assembler, an assembler
// than can aggregate multiple intents into a single funding transaction, or an
// external protocol that creates a funding output out-of-band such as channel
// factories.
type Assembler interface {
// ProvisionChannel returns a populated Intent that can be used to
// further the channel funding workflow. Depending on the
// implementation of Assembler, additional state machine (Intent)
// actions may be required before the FundingOutput and ChanPoint are
// made available to the caller.
ProvisionChannel(*Request) (Intent, error)
}
// FundingTxAssembler is a super-set of the regular Assembler interface that's
// also able to provide a fully populated funding transaction via the intents
// that it produuces.
type FundingTxAssembler interface {
Assembler
// FundingTxAvailable is an empty method that an assembler can
// implement to signal to callers that its able to provide the funding
// transaction for the channel via the intent it returns.
FundingTxAvailable()
}

@ -0,0 +1,187 @@
package chanfunding
import (
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
// ShimIntent is an intent created by the CannedAssembler which represents a
// funding output to be created that was constructed outside the wallet. This
// might be used when a hardware wallet, or a channel factory is the entity
// crafting the funding transaction, and not lnd.
type ShimIntent struct {
// localFundingAmt is the final amount we put into the funding output.
localFundingAmt btcutil.Amount
// remoteFundingAmt is the final amount the remote party put into the
// funding output.
remoteFundingAmt btcutil.Amount
// localKey is our multi-sig key.
localKey *keychain.KeyDescriptor
// remoteKey is the remote party's multi-sig key.
remoteKey *btcec.PublicKey
// chanPoint is the final channel point for the to be created channel.
chanPoint *wire.OutPoint
}
// FundingOutput returns the witness script, and the output that creates the
// funding output.
//
// NOTE: This method satisfies the chanfunding.Intent interface.
func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
if s.localKey == nil || s.remoteKey == nil {
return nil, nil, fmt.Errorf("unable to create witness " +
"script, no funding keys")
}
totalAmt := s.localFundingAmt + s.remoteFundingAmt
return input.GenFundingPkScript(
s.localKey.PubKey.SerializeCompressed(),
s.remoteKey.SerializeCompressed(),
int64(totalAmt),
)
}
// Cancel allows the caller to cancel a funding Intent at any time. This will
// return any resources such as coins back to the eligible pool to be used in
// order channel fundings.
//
// NOTE: This method satisfies the chanfunding.Intent interface.
func (s *ShimIntent) Cancel() {
}
// RemoteFundingAmt is the amount the remote party put into the channel.
//
// NOTE: This method satisfies the chanfunding.Intent interface.
func (s *ShimIntent) LocalFundingAmt() btcutil.Amount {
return s.localFundingAmt
}
// LocalFundingAmt is the amount we put into the channel. This may differ from
// the local amount requested, as depending on coin selection, we may bleed
// from of that LocalAmt into fees to minimize change.
//
// NOTE: This method satisfies the chanfunding.Intent interface.
func (s *ShimIntent) RemoteFundingAmt() btcutil.Amount {
return s.remoteFundingAmt
}
// ChanPoint returns the final outpoint that will create the funding output
// described above.
//
// NOTE: This method satisfies the chanfunding.Intent interface.
func (s *ShimIntent) ChanPoint() (*wire.OutPoint, error) {
if s.chanPoint == nil {
return nil, fmt.Errorf("chan point unknown, funding output " +
"not constructed")
}
return s.chanPoint, nil
}
// FundingKeys couples our multi-sig key along with the remote party's key.
type FundingKeys struct {
// LocalKey is our multi-sig key.
LocalKey *keychain.KeyDescriptor
// RemoteKey is the multi-sig key of the remote party.
RemoteKey *btcec.PublicKey
}
// MultiSigKeys returns the committed multi-sig keys, but only if they've been
// specified/provided.
func (s *ShimIntent) MultiSigKeys() (*FundingKeys, error) {
if s.localKey == nil || s.remoteKey == nil {
return nil, fmt.Errorf("unknown funding keys")
}
return &FundingKeys{
LocalKey: s.localKey,
RemoteKey: s.remoteKey,
}, nil
}
// A compile-time check to ensure ShimIntent adheres to the Intent interface.
var _ Intent = (*ShimIntent)(nil)
// CannedAssembler is a type of chanfunding.Assembler wherein the funding
// transaction is constructed outside of lnd, and may already exist. This
// Assembler serves as a shim which gives the funding flow the only thing it
// actually needs to proceed: the channel point.
type CannedAssembler struct {
// fundingAmt is the total amount of coins in the funding output.
fundingAmt btcutil.Amount
// localKey is our multi-sig key.
localKey *keychain.KeyDescriptor
// remoteKey is the remote party's multi-sig key.
remoteKey *btcec.PublicKey
// chanPoint is the final channel point for the to be created channel.
chanPoint wire.OutPoint
// initiator indicates if we're the initiator or the channel or not.
initiator bool
}
// NewCannedAssembler creates a new CannedAssembler from the material required
// to construct a funding output and channel point.
func NewCannedAssembler(chanPoint wire.OutPoint, fundingAmt btcutil.Amount,
localKey *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey, initiator bool) *CannedAssembler {
return &CannedAssembler{
initiator: initiator,
localKey: localKey,
remoteKey: remoteKey,
fundingAmt: fundingAmt,
chanPoint: chanPoint,
}
}
// ProvisionChannel creates a new ShimIntent given the passed funding Request.
// The returned intent is immediately able to provide the channel point and
// funding output as they've already been created outside lnd.
//
// NOTE: This method satisfies the chanfunding.Assembler interface.
func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
switch {
// A simple sanity check to ensure the provision request matches the
// re-made shim intent.
case req.LocalAmt != c.fundingAmt:
return nil, fmt.Errorf("intent doesn't match canned assembler")
// We'll exit out if this field is set as the funding transaction has
// already been assembled, so we don't influence coin selection..
case req.SubtractFees:
return nil, fmt.Errorf("SubtractFees ignored, funding " +
"transaction is frozen")
}
intent := &ShimIntent{
localKey: c.localKey,
remoteKey: c.remoteKey,
chanPoint: &c.chanPoint,
}
if c.initiator {
intent.localFundingAmt = c.fundingAmt
} else {
intent.remoteFundingAmt = c.fundingAmt
}
return intent, nil
}
// A compile-time assertion to ensure CannedAssembler meets the Assembler
// interface.
var _ Assembler = (*CannedAssembler)(nil)

@ -0,0 +1,29 @@
package chanfunding
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger("CHFD", nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

@ -0,0 +1,343 @@
package chanfunding
import (
"math"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/txsort"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
)
// FullIntent is an intent that is fully backed by the internal wallet. This
// intent differs from the ShimIntent, in that the funding transaction will be
// constructed internally, and will consist of only inputs we wholly control.
// This Intent implements a basic state machine that must be executed in order
// before CompileFundingTx can be called.
//
// Steps to final channel provisioning:
// 1. Call BindKeys to notify the intent which keys to use when constructing
// the multi-sig output.
// 2. Call CompileFundingTx afterwards to obtain the funding transaction.
//
// If either of these steps fail, then the Cancel method MUST be called.
type FullIntent struct {
ShimIntent
// InputCoins are the set of coins selected as inputs to this funding
// transaction.
InputCoins []Coin
// ChangeOutputs are the set of outputs that the Assembler will use as
// change from the main funding transaction.
ChangeOutputs []*wire.TxOut
// coinLocker is the Assembler's instance of the OutpointLocker
// interface.
coinLocker OutpointLocker
// coinSource is the Assembler's instance of the CoinSource interface.
coinSource CoinSource
// signer is the Assembler's instance of the Singer interface.
signer input.Signer
}
// BindKeys is a method unique to the FullIntent variant. This allows the
// caller to decide precisely which keys are used in the final funding
// transaction. This is kept out of the main Assembler as these may may not
// necessarily be under full control of the wallet. Only after this method has
// been executed will CompileFundingTx succeed.
func (f *FullIntent) BindKeys(localKey *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey) {
f.localKey = localKey
f.remoteKey = remoteKey
}
// CompileFundingTx is to be called after BindKeys on the sub-intent has been
// called. This method will construct the final funding transaction, and fully
// sign all inputs that are known by the backing CoinSource. After this method
// returns, the Intent is assumed to be complete, as the output can be created
// at any point.
func (f *FullIntent) CompileFundingTx(extraInputs []*wire.TxIn,
extraOutputs []*wire.TxOut) (*wire.MsgTx, error) {
// Create a blank, fresh transaction. Soon to be a complete funding
// transaction which will allow opening a lightning channel.
fundingTx := wire.NewMsgTx(2)
// Add all multi-party inputs and outputs to the transaction.
for _, coin := range f.InputCoins {
fundingTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: coin.OutPoint,
})
}
for _, theirInput := range extraInputs {
fundingTx.AddTxIn(theirInput)
}
for _, ourChangeOutput := range f.ChangeOutputs {
fundingTx.AddTxOut(ourChangeOutput)
}
for _, theirChangeOutput := range extraOutputs {
fundingTx.AddTxOut(theirChangeOutput)
}
_, fundingOutput, err := f.FundingOutput()
if err != nil {
return nil, err
}
// Sort the transaction. Since both side agree to a canonical ordering,
// by sorting we no longer need to send the entire transaction. Only
// signatures will be exchanged.
fundingTx.AddTxOut(fundingOutput)
txsort.InPlaceSort(fundingTx)
// Now that the funding tx has been fully assembled, we'll locate the
// index of the funding output so we can create our final channel
// point.
_, multiSigIndex := input.FindScriptOutputIndex(
fundingTx, fundingOutput.PkScript,
)
// Next, sign all inputs that are ours, collecting the signatures in
// order of the inputs.
signDesc := input.SignDescriptor{
HashType: txscript.SigHashAll,
SigHashes: txscript.NewTxSigHashes(fundingTx),
}
for i, txIn := range fundingTx.TxIn {
// We can only sign this input if it's ours, so we'll ask the
// coin source if it can map this outpoint into a coin we own.
// If not, then we'll continue as it isn't our input.
info, err := f.coinSource.CoinFromOutPoint(
txIn.PreviousOutPoint,
)
if err != nil {
continue
}
// Now that we know the input is ours, we'll populate the
// signDesc with the per input unique information.
signDesc.Output = &wire.TxOut{
Value: info.Value,
PkScript: info.PkScript,
}
signDesc.InputIndex = i
// Finally, we'll sign the input as is, and populate the input
// with the witness and sigScript (if needed).
inputScript, err := f.signer.ComputeInputScript(
fundingTx, &signDesc,
)
if err != nil {
return nil, err
}
txIn.SignatureScript = inputScript.SigScript
txIn.Witness = inputScript.Witness
}
// Finally, we'll populate the chanPoint now that we've fully
// constructed the funding transaction.
f.chanPoint = &wire.OutPoint{
Hash: fundingTx.TxHash(),
Index: multiSigIndex,
}
return fundingTx, nil
}
// Cancel allows the caller to cancel a funding Intent at any time. This will
// return any resources such as coins back to the eligible pool to be used in
// order channel fundings.
//
// NOTE: Part of the chanfunding.Intent interface.
func (f *FullIntent) Cancel() {
for _, coin := range f.InputCoins {
f.coinLocker.UnlockOutpoint(coin.OutPoint)
}
f.ShimIntent.Cancel()
}
// A compile-time check to ensure FullIntent meets the Intent interface.
var _ Intent = (*FullIntent)(nil)
// WalletConfig is the main config of the WalletAssembler.
type WalletConfig struct {
// CoinSource is what the WalletAssembler uses to list/locate coins.
CoinSource CoinSource
// CoinSelectionLocker allows the WalletAssembler to gain exclusive
// access to the current set of coins returned by the CoinSource.
CoinSelectLocker CoinSelectionLocker
// CoinLocker is what the WalletAssembler uses to lock coins that may
// be used as inputs for a new funding transaction.
CoinLocker OutpointLocker
// Signer allows the WalletAssembler to sign inputs on any potential
// funding transactions.
Signer input.Signer
// DustLimit is the current dust limit. We'll use this to ensure that
// we don't make dust outputs on the funding transaction.
DustLimit btcutil.Amount
}
// WalletAssembler is an instance of the Assembler interface that is backed by
// a full wallet. This variant of the Assembler interface will produce the
// entirety of the funding transaction within the wallet. This implements the
// typical funding flow that is initiated either on the p2p level or using the
// CLi.
type WalletAssembler struct {
cfg WalletConfig
}
// NewWalletAssembler creates a new instance of the WalletAssembler from a
// fully populated wallet config.
func NewWalletAssembler(cfg WalletConfig) *WalletAssembler {
return &WalletAssembler{
cfg: cfg,
}
}
// ProvisionChannel is the main entry point to begin a funding workflow given a
// fully populated request. The internal WalletAssembler will perform coin
// selection in a goroutine safe manner, returning an Intent that will allow
// the caller to finalize the funding process.
//
// NOTE: To cancel the funding flow the Cancel() method on the returned Intent,
// MUST be called.
//
// NOTE: This is a part of the chanfunding.Assembler interface.
func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
var intent Intent
// We hold the coin select mutex while querying for outputs, and
// performing coin selection in order to avoid inadvertent double
// spends across funding transactions.
err := w.cfg.CoinSelectLocker.WithCoinSelectLock(func() error {
log.Infof("Performing funding tx coin selection using %v "+
"sat/kw as fee rate", int64(r.FeeRate))
// Find all unlocked unspent witness outputs that satisfy the
// minimum number of confirmations required.
coins, err := w.cfg.CoinSource.ListCoins(
r.MinConfs, math.MaxInt32,
)
if err != nil {
return err
}
var (
selectedCoins []Coin
localContributionAmt btcutil.Amount
changeAmt btcutil.Amount
)
// Perform coin selection over our available, unlocked unspent
// outputs in order to find enough coins to meet the funding
// amount requirements.
switch {
// If there's no funding amount at all (receiving an inbound
// single funder request), then we don't need to perform any
// coin selection at all.
case r.LocalAmt == 0:
break
// In case this request want the fees subtracted from the local
// amount, we'll call the specialized method for that. This
// ensures that we won't deduct more that the specified balance
// from our wallet.
case r.SubtractFees:
dustLimit := w.cfg.DustLimit
selectedCoins, localContributionAmt, changeAmt, err = CoinSelectSubtractFees(
r.FeeRate, r.LocalAmt, dustLimit, coins,
)
if err != nil {
return err
}
// Otherwise do a normal coin selection where we target a given
// funding amount.
default:
localContributionAmt = r.LocalAmt
selectedCoins, changeAmt, err = CoinSelect(
r.FeeRate, r.LocalAmt, coins,
)
if err != nil {
return err
}
}
// Record any change output(s) generated as a result of the
// coin selection, but only if the addition of the output won't
// lead to the creation of dust.
var changeOutput *wire.TxOut
if changeAmt != 0 && changeAmt > w.cfg.DustLimit {
changeAddr, err := r.ChangeAddr()
if err != nil {
return err
}
changeScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil {
return err
}
changeOutput = &wire.TxOut{
Value: int64(changeAmt),
PkScript: changeScript,
}
}
// Lock the selected coins. These coins are now "reserved",
// this prevents concurrent funding requests from referring to
// and this double-spending the same set of coins.
for _, coin := range selectedCoins {
outpoint := coin.OutPoint
w.cfg.CoinLocker.LockOutpoint(outpoint)
}
newIntent := &FullIntent{
ShimIntent: ShimIntent{
localFundingAmt: localContributionAmt,
remoteFundingAmt: r.RemoteAmt,
},
InputCoins: selectedCoins,
coinLocker: w.cfg.CoinLocker,
coinSource: w.cfg.CoinSource,
signer: w.cfg.Signer,
}
if changeOutput != nil {
newIntent.ChangeOutputs = []*wire.TxOut{changeOutput}
}
intent = newIntent
return nil
})
if err != nil {
return nil, err
}
return intent, nil
}
// FundingTxAvailable is an empty method that an assembler can implement to
// signal to callers that its able to provide the funding transaction for the
// channel via the intent it returns.
//
// NOTE: This method is a part of the FundingTxAssembler interface.
func (w *WalletAssembler) FundingTxAvailable() {}
// A compile-time assertion to ensure the WalletAssembler meets the
// FundingTxAssembler interface.
var _ FundingTxAssembler = (*WalletAssembler)(nil)

2
log.go

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