Merge pull request #7516 from hieblmi/utxo-funding

funding: fund channel with selected utxos
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2023-07-25 19:19:42 +02:00
commit 9ed064be75
14 files changed
+2796 -2196

No files matched your search

+40
View File
@@ -122,6 +122,17 @@ var openChannelCommand = cli.Command{
"channel. This must not be set at the same " + "channel. This must not be set at the same " +
"time as local_amt", "time as local_amt",
}, },
cli.StringSliceFlag{
Name: "utxo",
Usage: "a utxo specified as outpoint(tx:idx) which " +
"will be used to fund a channel. This flag " +
"can be repeatedly used to fund a channel " +
"with a selection of utxos. The selected " +
"funds can either be entirely spent by " +
"specifying the fundmax flag or partially by " +
"selecting a fraction of the sum of the " +
"outpoints in local_amt",
},
cli.Uint64Flag{ cli.Uint64Flag{
Name: "base_fee_msat", Name: "base_fee_msat",
Usage: "the base fee in milli-satoshis that will " + Usage: "the base fee in milli-satoshis that will " +
@@ -391,6 +402,17 @@ func openChannel(ctx *cli.Context) error {
"to commit the maximum amount out of the wallet") "to commit the maximum amount out of the wallet")
} }
if ctx.IsSet("utxo") {
utxos := ctx.StringSlice("utxo")
outpoints, err := utxosToOutpoints(utxos)
if err != nil {
return fmt.Errorf("unable to decode utxos: %w", err)
}
req.Outpoints = outpoints
}
if ctx.IsSet("push_amt") { if ctx.IsSet("push_amt") {
req.PushSat = int64(ctx.Int("push_amt")) req.PushSat = int64(ctx.Int("push_amt"))
} else if args.Present() { } else if args.Present() {
@@ -1109,3 +1131,21 @@ func decodePsbt(psbt string) ([]byte, error) {
return nil, fmt.Errorf("not a PSBT") return nil, fmt.Errorf("not a PSBT")
} }
} }
// parseUtxos parses a comma separated list of utxos into outpoints that are
// passed to the server.
func utxosToOutpoints(utxos []string) ([]*lnrpc.OutPoint, error) {
var outpoints []*lnrpc.OutPoint
if len(utxos) == 0 {
return nil, fmt.Errorf("no utxos specified")
}
for _, utxo := range utxos {
outpoint, err := NewProtoOutPoint(utxo)
if err != nil {
return nil, err
}
outpoints = append(outpoints, outpoint)
}
return outpoints, nil
}
@@ -109,6 +109,10 @@ on the old value of 10000](https://github.com/lightningnetwork/lnd/pull/7780).
key scopes. The non-deterministic behaviours linked to this case are fixed, key scopes. The non-deterministic behaviours linked to this case are fixed,
and users can no longer create two custom accounts with the same name. and users can no longer create two custom accounts with the same name.
* `OpenChannel` adds a new `utxo` flag that allows the specification of multiple
UTXOs [as a basis for funding a channel
opening.](https://github.com/lightningnetwork/lnd/pull/7516)
## Misc ## Misc
* [Generate default macaroons * [Generate default macaroons
+9
View File
@@ -276,6 +276,13 @@ type InitFundingMsg struct {
// minimum amount to commit to. // minimum amount to commit to.
MinFundAmt btcutil.Amount MinFundAmt btcutil.Amount
// Outpoints is a list of client-selected outpoints that should be used
// for funding a channel. If LocalFundingAmt is specified then this
// amount is allocated from the sum of outpoints towards funding. If
// the FundUpToMaxAmt is specified the entirety of selected funds is
// allocated towards channel funding.
Outpoints []wire.OutPoint
// ChanFunder is an optional channel funder that allows the caller to // ChanFunder is an optional channel funder that allows the caller to
// control exactly how the channel funding is carried out. If not // control exactly how the channel funding is carried out. If not
// specified, then the default chanfunding.WalletAssembler will be // specified, then the default chanfunding.WalletAssembler will be
@@ -3973,6 +3980,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
maxHtlcs = msg.MaxHtlcs maxHtlcs = msg.MaxHtlcs
maxCSV = msg.MaxLocalCsv maxCSV = msg.MaxLocalCsv
chanReserve = msg.RemoteChanReserve chanReserve = msg.RemoteChanReserve
outpoints = msg.Outpoints
) )
// If no maximum CSV delay was set for this channel, we use our default // If no maximum CSV delay was set for this channel, we use our default
@@ -4101,6 +4109,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
FundUpToMaxAmt: msg.FundUpToMaxAmt, FundUpToMaxAmt: msg.FundUpToMaxAmt,
MinFundAmt: msg.MinFundAmt, MinFundAmt: msg.MinFundAmt,
RemoteChanReserve: chanReserve, RemoteChanReserve: chanReserve,
Outpoints: outpoints,
CommitFeePerKw: commitFeePerKw, CommitFeePerKw: commitFeePerKw,
FundingFeePerKw: msg.FundingFeePerKw, FundingFeePerKw: msg.FundingFeePerKw,
PushMSat: msg.PushAmt, PushMSat: msg.PushAmt,
+4
View File
@@ -542,4 +542,8 @@ var allTestCases = []*lntest.TestCase{
Name: "custom features", Name: "custom features",
TestFunc: testCustomFeatures, TestFunc: testCustomFeatures,
}, },
{
Name: "utxo selection funding",
TestFunc: testChannelUtxoSelection,
},
} }
@@ -0,0 +1,369 @@
package itest
import (
"context"
"fmt"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lnwallet"
)
type chanFundUtxoSelectionTestCase struct {
// name is the name of the target test case.
name string
// initialCoins are the initial coins in Alice's wallet.
initialCoins []btcutil.Amount
// selectedCoins are the coins alice is selecting for funding a channel.
selectedCoins []btcutil.Amount
// localAmt is the local portion of the channel funding amount.
localAmt btcutil.Amount
// pushAmt is the amount to be pushed to Bob.
pushAmt btcutil.Amount
// feeRate is an optional fee in satoshi/bytes used when opening a
// channel.
feeRate btcutil.Amount
// expectedBalance is Alice's expected balance in her channel.
expectedBalance btcutil.Amount
// remainingWalletBalance is Alice's expected remaining wallet balance
// after she opened a channgel.
remainingWalletBalance btcutil.Amount
// chanOpenShouldFail denotes if we expect the channel opening to fail.
chanOpenShouldFail bool
// expectedErrStr contains the expected error in case chanOpenShouldFail
// is set to true.
expectedErrStr string
// commitmentType allows to define the exact type when opening the
// channel.
commitmentType lnrpc.CommitmentType
// reuseUtxo tries to spent a previously spent output.
reuseUtxo bool
}
// testChannelUtxoSelection checks various channel funding scenarios where the
// user instructed the wallet to use a selection funds available in the wallet.
func testChannelUtxoSelection(ht *lntest.HarnessTest) {
// Create two new nodes that open a channel between each other for these
// tests.
args := lntest.NodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS)
alice := ht.NewNode("Alice", args)
defer ht.Shutdown(alice)
bob := ht.NewNode("Bob", args)
defer ht.Shutdown(bob)
// Ensure both sides are connected so the funding flow can be properly
// executed.
ht.EnsureConnected(alice, bob)
// Calculate reserve amount for one channel.
reserveResp, _ := alice.RPC.WalletKit.RequiredReserve(
context.Background(), &walletrpc.RequiredReserveRequest{
AdditionalPublicChannels: 1,
},
)
reserveAmount := btcutil.Amount(reserveResp.RequiredReserve)
var tcs = []*chanFundUtxoSelectionTestCase{
// Selected coins would leave a dust output after subtracting
// miner fees.
{
name: "fundmax, wallet amount is dust",
initialCoins: []btcutil.Amount{2_000},
selectedCoins: []btcutil.Amount{2_000},
chanOpenShouldFail: true,
feeRate: 15,
expectedErrStr: "output amount(0.00000174 BTC) after " +
"subtracting fees(0.00001826 BTC) below dust " +
"limit(0.0000033 BTC)",
},
// Selected coins don't cover the minimum channel size.
{
name: "fundmax, local amount < min chan " +
"size",
initialCoins: []btcutil.Amount{18_000},
selectedCoins: []btcutil.Amount{18_000},
feeRate: 1,
chanOpenShouldFail: true,
expectedErrStr: "available funds(0.00017877 BTC) " +
"below the minimum amount(0.0002 BTC)",
},
// The local amount exceeds the value of the selected coins.
{
name: "selected, local amount > " +
"selected amount",
initialCoins: []btcutil.Amount{100_000, 50_000},
selectedCoins: []btcutil.Amount{100_000},
localAmt: btcutil.Amount(210_337),
chanOpenShouldFail: true,
expectedErrStr: "not enough witness outputs to " +
"create funding transaction, need 0.00210337 " +
"BTC only have 0.001 BTC available",
},
// We are spending two selected coins partially out of three
// available in the wallet and expect a change output and the
// unselected coin as remaining wallet balance.
{
name: "selected, local amount > " +
"min chan size",
initialCoins: []btcutil.Amount{
200_000, 50_000, 100_000,
},
selectedCoins: []btcutil.Amount{
200_000, 100_000,
},
localAmt: btcutil.Amount(250_000),
expectedBalance: btcutil.Amount(250_000),
remainingWalletBalance: btcutil.Amount(350_000) -
btcutil.Amount(250_000) -
fundingFee(2, true),
},
// We are spending the entirety of two selected coins out of
// three available in the wallet and expect no change output and
// the unselected coin as remaining wallet balance.
{
name: "fundmax, local amount > min " +
"chan size",
initialCoins: []btcutil.Amount{
200_000, 100_000, 50_000,
},
selectedCoins: []btcutil.Amount{
200_000, 50_000,
},
expectedBalance: btcutil.Amount(200_000) +
btcutil.Amount(50_000) -
fundingFee(2, false),
remainingWalletBalance: btcutil.Amount(100_000),
},
// Select all coins in wallet and use the maximum available
// local amount to fund an anchor channel.
{
name: "selected, local amount leaves sufficient " +
"reserve",
initialCoins: []btcutil.Amount{
200_000, 100_000,
},
selectedCoins: []btcutil.Amount{200_000, 100_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
reserveAmount -
fundingFee(2, true),
expectedBalance: btcutil.Amount(300_000) -
reserveAmount -
fundingFee(2, true),
remainingWalletBalance: reserveAmount,
},
// Select all coins in wallet towards local amount except for a
// anchor reserve portion.
{
name: "selected, reserve from selected",
initialCoins: []btcutil.Amount{
200_000, reserveAmount, 100_000,
},
selectedCoins: []btcutil.Amount{
200_000, reserveAmount, 100_000,
},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
fundingFee(3, true),
expectedBalance: btcutil.Amount(300_000) -
fundingFee(3, true),
remainingWalletBalance: reserveAmount,
},
// Select all coins in wallet and use more than the maximum
// available local amount to fund an anchor channel.
{
name: "selected, local amount leaves insufficient " +
"reserve",
initialCoins: []btcutil.Amount{
200_000, 100_000,
},
selectedCoins: []btcutil.Amount{200_000, 100_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
localAmt: btcutil.Amount(300_000) -
reserveAmount + 1 -
fundingFee(2, true),
chanOpenShouldFail: true,
expectedErrStr: "reserved wallet balance " +
"invalidated: transaction would leave " +
"insufficient funds for fee bumping anchor " +
"channel closings",
},
// We fund an anchor channel with a single coin and just keep
// enough funds in the wallet to cover for the anchor reserve.
{
name: "fundmax, sufficient reserve",
initialCoins: []btcutil.Amount{
200_000, reserveAmount,
},
selectedCoins: []btcutil.Amount{200_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
expectedBalance: btcutil.Amount(200_000) -
fundingFee(1, false),
remainingWalletBalance: reserveAmount,
},
// We fund an anchor channel with a single coin and expect the
// reserve amount left in the wallet.
{
name: "fundmax, sufficient reserve from channel " +
"balance carve out",
initialCoins: []btcutil.Amount{
200_000,
},
selectedCoins: []btcutil.Amount{200_000},
commitmentType: lnrpc.CommitmentType_ANCHORS,
expectedBalance: btcutil.Amount(200_000) -
reserveAmount -
fundingFee(1, true),
remainingWalletBalance: reserveAmount,
},
// Confirm that already spent outputs can't be reused to fund
// another channel.
{
name: "output already spent",
initialCoins: []btcutil.Amount{
200_000,
},
selectedCoins: []btcutil.Amount{200_000},
reuseUtxo: true,
},
}
for _, tc := range tcs {
success := ht.Run(
tc.name, func(tt *testing.T) {
runUtxoSelectionTestCase(
ht, tt, alice, bob, tc,
reserveAmount,
)
},
)
// Stop at the first failure. Mimic behavior of original test
if !success {
break
}
}
}
// runUtxoSelectionTestCase runs a single test case asserting that test
// conditions are met.
func runUtxoSelectionTestCase(ht *lntest.HarnessTest, t *testing.T, alice,
bob *node.HarnessNode, tc *chanFundUtxoSelectionTestCase,
reserveAmount btcutil.Amount) {
// fund initial coins
for _, initialCoin := range tc.initialCoins {
ht.FundCoins(initialCoin, alice)
}
defer func() {
// Fund additional coins to sweep in case the wallet contains
// dust.
ht.FundCoins(100_000, alice)
// Remove all funds from Alice.
sweepNodeWalletAndAssert(ht, alice)
}()
// Create an outpoint lookup for each unique amount.
lookup := make(map[int64]*lnrpc.OutPoint)
res := alice.RPC.ListUnspent(&walletrpc.ListUnspentRequest{})
for _, utxo := range res.Utxos {
lookup[utxo.AmountSat] = utxo.Outpoint
}
// Map the selected coin to the respective outpoint.
selectedOutpoints := []*lnrpc.OutPoint{}
for _, selectedCoin := range tc.selectedCoins {
if outpoint, ok := lookup[int64(selectedCoin)]; ok {
selectedOutpoints = append(
selectedOutpoints, outpoint,
)
}
}
commitType := tc.commitmentType
if commitType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
commitType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
}
// The parameters to try opening the channel with.
fundMax := false
if tc.localAmt == 0 {
fundMax = true
}
chanParams := lntest.OpenChannelParams{
Amt: tc.localAmt,
FundMax: fundMax,
PushAmt: tc.pushAmt,
CommitmentType: commitType,
SatPerVByte: tc.feeRate,
Outpoints: selectedOutpoints,
}
// If we don't expect the channel opening to be
// successful, simply check for an error.
if tc.chanOpenShouldFail {
expectedErr := fmt.Errorf(tc.expectedErrStr)
ht.OpenChannelAssertErr(
alice, bob, chanParams, expectedErr,
)
return
}
// Otherwise, if we expect to open a channel use the helper function.
chanPoint := ht.OpenChannel(alice, bob, chanParams)
defer ht.CloseChannel(alice, chanPoint)
// When re-selecting a spent output for funding another channel we
// expect the respective error message.
if tc.reuseUtxo {
expectedErrStr := fmt.Sprintf("outpoint already spent: %s:%d",
selectedOutpoints[0].TxidStr,
selectedOutpoints[0].OutputIndex)
expectedErr := fmt.Errorf(expectedErrStr)
ht.OpenChannelAssertErr(
alice, bob, chanParams, expectedErr,
)
return
}
cType := ht.GetChannelCommitType(alice, chanPoint)
// Alice's balance should be her amount subtracted by the commitment
// transaction fee.
checkChannelBalance(
ht, alice, tc.expectedBalance-lntest.CalcStaticFee(cType, 0),
tc.pushAmt,
)
// Ensure Bob's balance within the channel is equal to the push amount.
checkChannelBalance(
ht, bob, tc.pushAmt,
tc.expectedBalance-lntest.CalcStaticFee(cType, 0),
)
ht.AssertWalletAccountBalance(
alice, lnwallet.DefaultAccountName,
int64(tc.remainingWalletBalance),
0,
)
}
+2206 -2193
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -2290,6 +2290,11 @@ message OpenChannelRequest {
the channel's operation. the channel's operation.
*/ */
string memo = 27; string memo = 27;
/*
A list of selected outpoints that are allocated for channel funding.
*/
repeated OutPoint outpoints = 28;
} }
message OpenStatusUpdate { message OpenStatusUpdate {
oneof update { oneof update {
+7
View File
@@ -5837,6 +5837,13 @@
"memo": { "memo": {
"type": "string", "type": "string",
"description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation." "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation."
},
"outpoints": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcOutPoint"
},
"description": "A list of selected outpoints that are allocated for channel funding."
} }
} }
}, },
+8
View File
@@ -896,6 +896,13 @@ type OpenChannelParams struct {
// has no bearing on the channel's operation. Max allowed length is 500 // has no bearing on the channel's operation. Max allowed length is 500
// characters. // characters.
Memo string Memo string
// Outpoints is a list of client-selected outpoints that should be used
// for funding a channel. If Amt is specified then this amount is
// allocated from the sum of outpoints towards funding. If the
// FundMax flag is specified the entirety of selected funds is
// allocated towards channel funding.
Outpoints []*lnrpc.OutPoint
} }
// prepareOpenChannel waits for both nodes to be synced to chain and returns an // prepareOpenChannel waits for both nodes to be synced to chain and returns an
@@ -938,6 +945,7 @@ func (h *HarnessTest) prepareOpenChannel(srcNode, destNode *node.HarnessNode,
UseFeeRate: p.UseFeeRate, UseFeeRate: p.UseFeeRate,
FundMax: p.FundMax, FundMax: p.FundMax,
Memo: p.Memo, Memo: p.Memo,
Outpoints: p.Outpoints,
} }
} }
+7
View File
@@ -84,6 +84,13 @@ type Request struct {
// e.g. anchor channels. // e.g. anchor channels.
WalletReserve btcutil.Amount WalletReserve btcutil.Amount
// Outpoints is a list of client-selected outpoints that should be used
// for funding a channel. If LocalAmt is specified then this amount is
// allocated from the sum of outpoints towards funding. If the
// FundUpToMaxAmt is specified the entirety of selected funds is
// allocated towards channel funding.
Outpoints []wire.OutPoint
// MinConfs controls how many confirmations a coin need to be eligible // 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 // to be used as an input to the funding transaction. If this value is
// set to zero, then zero conf outputs may be spent. // set to zero, then zero conf outputs may be spent.
+1 -1
View File
@@ -22,7 +22,7 @@ type ErrInsufficientFunds struct {
// Error returns a human readable string describing the error. // Error returns a human readable string describing the error.
func (e *ErrInsufficientFunds) Error() string { func (e *ErrInsufficientFunds) Error() string {
return fmt.Sprintf("not enough witness outputs to create funding "+ return fmt.Sprintf("not enough witness outputs to create funding "+
"transaction, need %v only have %v available", "transaction, need %v only have %v available",
e.amountAvailable, e.amountSelected) e.amountAvailable, e.amountSelected)
} }
+99 -2
View File
@@ -260,23 +260,64 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
log.Infof("Performing funding tx coin selection using %v "+ log.Infof("Performing funding tx coin selection using %v "+
"sat/kw as fee rate", int64(r.FeeRate)) "sat/kw as fee rate", int64(r.FeeRate))
var (
// allCoins refers to the entirety of coins in our
// wallet that are available for funding a channel.
allCoins []Coin
// manuallySelectedCoins refers to the client-side
// selected coins that should be considered available
// for funding a channel.
manuallySelectedCoins []Coin
err error
)
// Convert manually selected outpoints to coins.
manuallySelectedCoins, err = outpointsToCoins(
r.Outpoints, w.cfg.CoinSource.CoinFromOutPoint,
)
if err != nil {
return err
}
// Find all unlocked unspent witness outputs that satisfy the // Find all unlocked unspent witness outputs that satisfy the
// minimum number of confirmations required. Coin selection in // minimum number of confirmations required. Coin selection in
// this function currently ignores the configured coin selection // this function currently ignores the configured coin selection
// strategy. // strategy.
coins, err := w.cfg.CoinSource.ListCoins( allCoins, err = w.cfg.CoinSource.ListCoins(
r.MinConfs, math.MaxInt32, r.MinConfs, math.MaxInt32,
) )
if err != nil { if err != nil {
return err return err
} }
// Ensure that all manually selected coins remain unspent.
unspent := make(map[wire.OutPoint]struct{})
for _, coin := range allCoins {
unspent[coin.OutPoint] = struct{}{}
}
for _, coin := range manuallySelectedCoins {
if _, ok := unspent[coin.OutPoint]; !ok {
return fmt.Errorf("outpoint already spent: %v",
coin.OutPoint)
}
}
var ( var (
coins []Coin
selectedCoins []Coin selectedCoins []Coin
localContributionAmt btcutil.Amount localContributionAmt btcutil.Amount
changeAmt btcutil.Amount changeAmt btcutil.Amount
) )
// If outputs were specified manually then we'll take the
// corresponding coins as basis for coin selection. Otherwise,
// all available coins from our wallet are used.
coins = allCoins
if len(manuallySelectedCoins) > 0 {
coins = manuallySelectedCoins
}
// Perform coin selection over our available, unlocked unspent // Perform coin selection over our available, unlocked unspent
// outputs in order to find enough coins to meet the funding // outputs in order to find enough coins to meet the funding
// amount requirements. // amount requirements.
@@ -305,10 +346,45 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// we will call the specialized coin selection function for // we will call the specialized coin selection function for
// that. // that.
case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0: case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0:
// We need to ensure that manually selected coins, which
// are spent entirely on the channel funding, leave
// enough funds in the wallet to cover for a reserve.
reserve := r.WalletReserve
if len(manuallySelectedCoins) > 0 {
sumCoins := func(coins []Coin) btcutil.Amount {
var sum btcutil.Amount
for _, coin := range coins {
sum += btcutil.Amount(
coin.Value,
)
}
return sum
}
sumManual := sumCoins(manuallySelectedCoins)
sumAll := sumCoins(allCoins)
// If sufficient reserve funds are available we
// don't have to provide for it during coin
// selection. The manually selected coins can be
// spent entirely on the channel funding. If
// the excess of coins cover the reserve
// partially then we have to provide for the
// rest during coin selection.
excess := sumAll - sumManual
if excess >= reserve {
reserve = 0
} else {
reserve -= excess
}
}
selectedCoins, localContributionAmt, changeAmt, selectedCoins, localContributionAmt, changeAmt,
err = CoinSelectUpToAmount( err = CoinSelectUpToAmount(
r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt, r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt,
r.WalletReserve, w.cfg.DustLimit, coins, reserve, w.cfg.DustLimit, coins,
) )
if err != nil { if err != nil {
return err return err
@@ -422,6 +498,27 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
return intent, nil return intent, nil
} }
// outpointsToCoins maps outpoints to coins in our wallet iff these coins are
// existent and returns an error otherwise.
func outpointsToCoins(outpoints []wire.OutPoint,
coinFromOutPoint func(wire.OutPoint) (*Coin, error)) ([]Coin, error) {
var selectedCoins []Coin
for _, outpoint := range outpoints {
coin, err := coinFromOutPoint(
outpoint,
)
if err != nil {
return nil, err
}
selectedCoins = append(
selectedCoins, *coin,
)
}
return selectedCoins, nil
}
// FundingTxAvailable is an empty method that an assembler can implement to // FundingTxAvailable is an empty method that an assembler can implement to
// signal to callers that its able to provide the funding transaction for the // signal to callers that its able to provide the funding transaction for the
// channel via the intent it returns. // channel via the intent it returns.
+8
View File
@@ -131,6 +131,13 @@ type InitFundingReserveMsg struct {
// allocated iff the FundUpToMaxAmt is set. // allocated iff the FundUpToMaxAmt is set.
MinFundAmt btcutil.Amount MinFundAmt btcutil.Amount
// Outpoints is a list of client-selected outpoints that should be used
// for funding a channel. If LocalFundingAmt is specified then this
// amount is allocated from the sum of outpoints towards funding. If the
// FundUpToMaxAmt is specified the entirety of selected funds is
// allocated towards channel funding.
Outpoints []wire.OutPoint
// RemoteChanReserve is the channel reserve we required for the remote // RemoteChanReserve is the channel reserve we required for the remote
// peer. // peer.
RemoteChanReserve btcutil.Amount RemoteChanReserve btcutil.Amount
@@ -899,6 +906,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
WalletReserve: l.RequiredReserve( WalletReserve: l.RequiredReserve(
uint32(numAnchorChans), uint32(numAnchorChans),
), ),
Outpoints: req.Outpoints,
MinConfs: req.MinConfs, MinConfs: req.MinConfs,
SubtractFees: req.SubtractFees, SubtractFees: req.SubtractFees,
FeeRate: req.FundingFeePerKw, FeeRate: req.FundingFeePerKw,
+29
View File
@@ -2145,6 +2145,15 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
"exceeds %d", in.Memo, len(in.Memo), maxMemoLength) "exceeds %d", in.Memo, len(in.Memo), maxMemoLength)
} }
// Check, if manually selected outpoints are present to fund a channel.
var outpoints []wire.OutPoint
if len(in.Outpoints) > 0 {
outpoints, err = toWireOutpoints(in.Outpoints)
if err != nil {
return nil, fmt.Errorf("can't create outpoints %w", err)
}
}
// Instruct the server to trigger the necessary events to attempt to // Instruct the server to trigger the necessary events to attempt to
// open a new channel. A stream is returned in place, this stream will // open a new channel. A stream is returned in place, this stream will
// be used to consume updates of the state of the pending channel. // be used to consume updates of the state of the pending channel.
@@ -2171,9 +2180,29 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
FundUpToMaxAmt: fundUpToMaxAmt, FundUpToMaxAmt: fundUpToMaxAmt,
MinFundAmt: minFundAmt, MinFundAmt: minFundAmt,
Memo: []byte(in.Memo), Memo: []byte(in.Memo),
Outpoints: outpoints,
}, nil }, nil
} }
// toWireOutpoints converts a list of outpoints from the rpc format to the wire
// format.
func toWireOutpoints(outpoints []*lnrpc.OutPoint) ([]wire.OutPoint, error) {
var wireOutpoints []wire.OutPoint
for _, outpoint := range outpoints {
hash, err := chainhash.NewHashFromStr(outpoint.TxidStr)
if err != nil {
return nil, fmt.Errorf("cannot create chainhash")
}
wireOutpoint := wire.NewOutPoint(
hash, outpoint.OutputIndex,
)
wireOutpoints = append(wireOutpoints, *wireOutpoint)
}
return wireOutpoints, nil
}
// OpenChannel attempts to open a singly funded channel specified in the // OpenChannel attempts to open a singly funded channel specified in the
// request to a remote peer. // request to a remote peer.
func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest, func (r *rpcServer) OpenChannel(in *lnrpc.OpenChannelRequest,