Merge pull request #7516 from hieblmi/utxo-funding
funding: fund channel with selected utxos
This commit is contained in:
14 files changed
+2796
-2196
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user