Merge pull request #6903 from hieblmi/fundmax-flag

funding: add `fundmax` flag to `OpenChannelRequest`
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2023-04-06 10:57:29 -07:00
commit 591ff120b6
19 files changed
+3110 -1997

No files matched your search

+25 -2
View File
@@ -110,6 +110,13 @@ var openChannelCommand = cli.Command{
Usage: "the number of satoshis the wallet should " +
"commit to the channel",
},
cli.BoolFlag{
Name: "fundmax",
Usage: "if set, the wallet will attempt to commit " +
"the maximum possible local amount to the " +
"channel. This must not be set at the same " +
"time as local_amt",
},
cli.Uint64Flag{
Name: "base_fee_msat",
Usage: "the base fee in milli-satoshis that will " +
@@ -292,6 +299,7 @@ func openChannel(ctx *cli.Context) error {
ZeroConf: ctx.Bool("zero_conf"),
ScidAlias: ctx.Bool("scid_alias"),
RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"),
FundMax: ctx.Bool("fundmax"),
}
switch {
@@ -350,8 +358,23 @@ func openChannel(ctx *cli.Context) error {
return fmt.Errorf("unable to decode local amt: %v", err)
}
args = args.Tail()
default:
return fmt.Errorf("local amt argument missing")
case !ctx.Bool("fundmax"):
return fmt.Errorf("either local_amt or fundmax must be " +
"specified")
}
// The fundmax flag is NOT allowed to be combined with local_amt above.
// It is allowed to be combined with push_amt, but only if explicitly
// set.
if ctx.Bool("fundmax") && req.LocalFundingAmount != 0 {
return fmt.Errorf("local amount cannot be set if attempting " +
"to commit the maximum amount out of the wallet")
}
// The fundmax flag is NOT allowed to be combined with the psbt flag.
if ctx.Bool("fundmax") && ctx.Bool("psbt") {
return fmt.Errorf("psbt cannot be set if attempting " +
"to commit the maximum amount out of the wallet")
}
if ctx.IsSet("push_amt") {
@@ -12,6 +12,10 @@
restore single channels](https://github.com/lightningnetwork/lnd/pull/7437)
to and from a file on disk.
* [Add a `fundmax` flag to `openchannel` to allow for the allocation of all
funds in a wallet](https://github.com/lightningnetwork/lnd/pull/6903) towards
a new channel opening.
## Watchtowers
* [Fix Address iterator
@@ -66,6 +70,7 @@ available](https://github.com/lightningnetwork/lnd/pull/7529).
* ardevd
* Elle Mouton
* hieblmi
* Oliver Gugger
* Tommy Volk
* Yong Yu
+29 -17
View File
@@ -266,6 +266,15 @@ type InitFundingMsg struct {
// peer.
MaxLocalCsv uint16
// FundUpToMaxAmt is the maximum amount to try to commit to. If set, the
// MinFundAmt field denotes the acceptable minimum amount to commit to,
// while trying to commit as many coins as possible up to this value.
FundUpToMaxAmt btcutil.Amount
// MinFundAmt must be set iff FundUpToMaxAmt is set. It denotes the
// minimum amount to commit to.
MinFundAmt btcutil.Amount
// ChanFunder is an optional channel funder that allows the caller to
// control exactly how the channel funding is carried out. If not
// specified, then the default chanfunding.WalletAssembler will be
@@ -4079,23 +4088,26 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
}
req := &lnwallet.InitFundingReserveMsg{
ChainHash: &msg.ChainHash,
PendingChanID: chanID,
NodeID: peerKey,
NodeAddr: msg.Peer.Address(),
SubtractFees: msg.SubtractFees,
LocalFundingAmt: localAmt,
RemoteFundingAmt: 0,
CommitFeePerKw: commitFeePerKw,
FundingFeePerKw: msg.FundingFeePerKw,
PushMSat: msg.PushAmt,
Flags: channelFlags,
MinConfs: msg.MinConfs,
CommitType: commitType,
ChanFunder: msg.ChanFunder,
ZeroConf: zeroConf,
OptionScidAlias: scid,
ScidAliasFeature: scidFeatureVal,
ChainHash: &msg.ChainHash,
PendingChanID: chanID,
NodeID: peerKey,
NodeAddr: msg.Peer.Address(),
SubtractFees: msg.SubtractFees,
LocalFundingAmt: localAmt,
RemoteFundingAmt: 0,
FundUpToMaxAmt: msg.FundUpToMaxAmt,
MinFundAmt: msg.MinFundAmt,
RemoteChanReserve: chanReserve,
CommitFeePerKw: commitFeePerKw,
FundingFeePerKw: msg.FundingFeePerKw,
PushMSat: msg.PushAmt,
Flags: channelFlags,
MinConfs: msg.MinConfs,
CommitType: commitType,
ChanFunder: msg.ChanFunder,
ZeroConf: zeroConf,
OptionScidAlias: scid,
ScidAliasFeature: scidFeatureVal,
}
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
+191 -5
View File
@@ -710,7 +710,7 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
*wire.OutPoint, *wire.MsgTx) {
publ := fundChannel(
t, alice, bob, localFundingAmt, pushAmt, false, numConfs,
t, alice, bob, localFundingAmt, pushAmt, false, 0, 0, numConfs,
updateChan, announceChan, nil,
)
fundingOutPoint := &wire.OutPoint{
@@ -723,7 +723,8 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
// fundChannel takes the funding process to the point where the funding
// transaction is confirmed on-chain. Returns the funding tx.
func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
pushAmt btcutil.Amount, subtractFees bool, numConfs uint32,
pushAmt btcutil.Amount, subtractFees bool, fundUpToMaxAmt,
minFundAmt btcutil.Amount, numConfs uint32, //nolint:unparam
updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool,
chanType *lnwire.ChannelType) *wire.MsgTx {
@@ -734,6 +735,8 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
TargetPubkey: bob.privKey.PubKey(),
ChainHash: *fundingNetParams.GenesisHash,
SubtractFees: subtractFees,
FundUpToMaxAmt: fundUpToMaxAmt,
MinFundAmt: minFundAmt,
LocalFundingAmt: localFundingAmt,
PushAmt: lnwire.NewMSatFromSatoshis(pushAmt),
FundingFeePerKw: 1000,
@@ -3730,7 +3733,7 @@ func TestFundingManagerFundAll(t *testing.T) {
// Initiate a fund channel, and inspect the funding tx.
pushAmt := btcutil.Amount(0)
fundingTx := fundChannel(
t, alice, bob, test.spendAmt, pushAmt, true, 1,
t, alice, bob, test.spendAmt, pushAmt, true, 0, 0, 1,
updateChan, true, nil,
)
@@ -3761,6 +3764,157 @@ func TestFundingManagerFundAll(t *testing.T) {
}
}
// TestFundingManagerFundMax tests that we can initiate a funding request to use
// the maximum allowed funds remaining in the wallet.
func TestFundingManagerFundMax(t *testing.T) {
t.Parallel()
// Helper function to create a test utxos
constructTestUtxos := func(values ...btcutil.Amount) []*lnwallet.Utxo {
var utxos []*lnwallet.Utxo
for _, value := range values {
utxos = append(utxos, &lnwallet.Utxo{
AddressType: lnwallet.WitnessPubKey,
Value: value,
PkScript: mock.CoinPkScript,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{},
Index: 0,
},
})
}
return utxos
}
tests := []struct {
name string
coins []*lnwallet.Utxo
fundUpToMaxAmt btcutil.Amount
minFundAmt btcutil.Amount
pushAmt btcutil.Amount
change bool
}{
{
// We will spend all the funds in the wallet, and expect
// no change output due to the dust limit.
coins: constructTestUtxos(
MaxBtcFundingAmount + 1,
),
fundUpToMaxAmt: MaxBtcFundingAmount,
minFundAmt: MinChanFundingSize,
pushAmt: 0,
change: false,
},
{
// We spend less than the funds in the wallet, so a
// change output should be created.
coins: constructTestUtxos(
2 * MaxBtcFundingAmount,
),
fundUpToMaxAmt: MaxBtcFundingAmount,
minFundAmt: MinChanFundingSize,
pushAmt: 0,
change: true,
},
{
// We spend less than the funds in the wallet when
// setting a smaller channel size, so a change output
// should be created.
coins: constructTestUtxos(
MaxBtcFundingAmount,
),
fundUpToMaxAmt: MaxBtcFundingAmount / 2,
minFundAmt: MinChanFundingSize,
pushAmt: 0,
change: true,
},
{
// We are using the entirety of two inputs for the
// funding of a channel, hence expect no change output.
coins: constructTestUtxos(
MaxBtcFundingAmount/2, MaxBtcFundingAmount/2,
),
fundUpToMaxAmt: MaxBtcFundingAmount,
minFundAmt: MinChanFundingSize,
pushAmt: 0,
change: false,
},
{
// We are using a fraction of two inputs for the funding
// of our channel, hence expect a change output.
coins: constructTestUtxos(
MaxBtcFundingAmount/2, MaxBtcFundingAmount/2,
),
fundUpToMaxAmt: MaxBtcFundingAmount / 2,
minFundAmt: MinChanFundingSize,
pushAmt: 0,
change: true,
},
{
// We are funding a channel with half of the balance in
// our wallet hence expect a change output. Furthermore
// we push half of the funding amount to the remote end
// which we expect to succeed.
coins: constructTestUtxos(MaxBtcFundingAmount),
fundUpToMaxAmt: MaxBtcFundingAmount / 2,
minFundAmt: MinChanFundingSize / 4,
pushAmt: MaxBtcFundingAmount / 4,
change: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// We set up our mock wallet to control a list of UTXOs
// that sum to more than the max channel size.
addFunds := func(fundingCfg *Config) {
wc := fundingCfg.Wallet.WalletController
mockWc, ok := wc.(*mock.WalletController)
if ok {
mockWc.Utxos = test.coins
}
}
alice, bob := setupFundingManagers(t, addFunds)
defer tearDownFundingManagers(t, alice, bob)
// We will consume the channel updates as we go, so no
// buffering is needed.
updateChan := make(chan *lnrpc.OpenStatusUpdate)
// Initiate a fund channel, and inspect the funding tx.
pushAmt := test.pushAmt
fundingTx := fundChannel(
t, alice, bob, 0, pushAmt, false,
test.fundUpToMaxAmt, test.minFundAmt, 1,
updateChan, true, nil,
)
// Check whether the expected change output is present.
if test.change {
require.EqualValues(t, 2, len(fundingTx.TxOut))
}
if !test.change {
require.EqualValues(t, 1, len(fundingTx.TxOut))
}
// Inputs should be all funds in the wallet.
require.Equal(t, len(test.coins), len(fundingTx.TxIn))
for i, txIn := range fundingTx.TxIn {
require.Equal(
t, test.coins[i].OutPoint,
txIn.PreviousOutPoint,
)
}
})
}
}
// TestGetUpfrontShutdown tests different combinations of inputs for getting a
// shutdown script. It varies whether the peer has the feature set, whether
// the user has provided a script and our local configuration to test that
@@ -4212,8 +4366,8 @@ func TestFundingManagerZeroConf(t *testing.T) {
// Call fundChannel with the zero-conf ChannelType.
fundingTx := fundChannel(
t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true,
&channelType,
t, alice, bob, fundingAmt, pushAmt, false, 0, 0, 1, updateChan,
true, &channelType,
)
fundingOp := &wire.OutPoint{
Hash: fundingTx.TxHash(),
@@ -4309,3 +4463,35 @@ func TestFundingManagerZeroConf(t *testing.T) {
// have been deleted from the database, as the channel is announced.
assertNoFwdingPolicy(t, alice, bob, fundingOp)
}
// TestCommitmentTypeFundmaxSanityCheck was introduced as a way of reminding
// developers of new channel commitment types to also consider the channel
// opening behavior with a specified fundmax flag. To give a hypothetical
// example, if ANCHOR types had been introduced after the fundmax flag had been
// activated, the developer would have had to code for the anchor reserve in the
// funding manager in the context of public and private channels. Otherwise
// inconsistent bahvior would have resulted when specifying fundmax for
// different types of channel openings.
// To ensure consistency this test compares a map of locally defined channel
// commitment types to the list of channel types that are defined in the proto
// files. It fails if the proto files contain additional commitment types. Once
// the developer considered the new channel type behavior it can be added in
// this test to the map `allCommitmentTypes`.
func TestCommitmentTypeFundmaxSanityCheck(t *testing.T) {
t.Parallel()
allCommitmentTypes := map[string]int{
"UNKNOWN_COMMITMENT_TYPE": 0,
"LEGACY": 1,
"STATIC_REMOTE_KEY": 2,
"ANCHORS": 3,
"SCRIPT_ENFORCED_LEASE": 4,
}
for commitmentType := range lnrpc.CommitmentType_value {
if _, ok := allCommitmentTypes[commitmentType]; !ok {
t.Fatalf("Commitment type %s hasn't been considered "+
"in the context of the --fundmax flag for "+
"channel openings.", commitmentType)
}
}
}
+4
View File
@@ -519,4 +519,8 @@ var allTestCases = []*lntest.TestCase{
Name: "watchtower session management",
TestFunc: testWatchtowerSessionManagement,
},
{
Name: "channel fundmax",
TestFunc: testChannelFundMax,
},
}
+336
View File
@@ -0,0 +1,336 @@
package itest
import (
"context"
"fmt"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/input"
"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"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
type chanFundMaxTestCase struct {
// name is the name of the target test case.
name string
// initialWalletBalance is the amount in Alice's wallet.
initialWalletBalance 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
// expectedBalanceAlice is Alice's expected balance in her channel.
expectedBalanceAlice 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
// private denotes if the channel opening is announced to the network or
// not.
private bool
}
// testChannelFundMax checks various channel funding scenarios where the user
// instructed the wallet to use all remaining funds.
func testChannelFundMax(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 testCases = []*chanFundMaxTestCase{
{
name: "wallet amount is dust",
initialWalletBalance: 2_000,
chanOpenShouldFail: true,
feeRate: 20,
expectedErrStr: "output amount(-0.00000435 BTC) " +
"after subtracting fees(0.00002435 BTC) " +
"below dust limit(0.0000033 BTC)",
},
{
name: "wallet amount < min chan size " +
"(~18000sat)",
initialWalletBalance: 18_000,
// Using a feeRate of 1 sat/vByte ensures that we test
// for min chan size and not excessive fees.
feeRate: 1,
chanOpenShouldFail: true,
expectedErrStr: "available funds(0.00017877 BTC) " +
"below the minimum amount(0.0002 BTC)",
},
{
name: "wallet amount > min chan " +
"size (37000sat)",
initialWalletBalance: 37_000,
// The transaction fee to open the channel must be
// subtracted from Alice's balance.
// (since wallet balance < max-chan-size)
expectedBalanceAlice: btcutil.Amount(37_000) -
fundingFee(1, false),
},
{
name: "wallet amount > max chan size " +
"(20000000sat)",
initialWalletBalance: 20_000_000,
expectedBalanceAlice: lnd.MaxFundingAmount,
},
// Expects, that if the maximum funding amount for a channel is
// pushed to the remote side, then the funding flow is failing
// because the push amount has to be less than the local channel
// amount.
{
name: "wallet amount > max chan size, " +
"push amount == max-chan-size",
initialWalletBalance: 20_000_000,
pushAmt: lnd.MaxFundingAmount,
chanOpenShouldFail: true,
expectedErrStr: "amount pushed to remote peer for " +
"initial state must be below the local " +
"funding amount",
},
// Expects that if the maximum funding amount for a channel is
// pushed to the remote side then the funding flow is failing
// due to insufficient funds in the local balance to cover for
// fees in the channel opening. By that the test also ensures
// that the fees are not covered by the remaining wallet
// balance.
{
name: "wallet amount > max chan size, " +
"push amount == max-chan-size - 1_000",
initialWalletBalance: 20_000_000,
pushAmt: lnd.MaxFundingAmount - 1_000,
chanOpenShouldFail: true,
expectedErrStr: "funder balance too small (-8050000) " +
"with fee=9050 sat, minimum=708 sat required",
},
{
name: "wallet amount > max chan size, " +
"push amount 16766000",
initialWalletBalance: 20_000_000,
pushAmt: 16_766_000,
expectedBalanceAlice: lnd.MaxFundingAmount - 16_766_000,
},
{
name: "anchor reserved value",
initialWalletBalance: 100_000,
commitmentType: lnrpc.CommitmentType_ANCHORS,
expectedBalanceAlice: btcutil.Amount(100_000) -
fundingFee(1, true) - reserveAmount,
},
// Funding a private anchor channel should omit the achor
// reserve and produce no change output.
{
name: "private anchor no reserved " +
"value",
private: true,
initialWalletBalance: 100_000,
commitmentType: lnrpc.CommitmentType_ANCHORS,
expectedBalanceAlice: btcutil.Amount(100_000) -
fundingFee(1, false),
},
}
for _, testCase := range testCases {
success := ht.Run(
testCase.name, func(tt *testing.T) {
runFundMaxTestCase(
ht, tt, alice, bob, testCase,
reserveAmount,
)
},
)
// Stop at the first failure. Mimic behavior of original test
// framework.
if !success {
break
}
}
}
// runTestCase runs a single test case asserting that test conditions are met.
func runFundMaxTestCase(ht *lntest.HarnessTest, t *testing.T, alice,
bob *node.HarnessNode, testCase *chanFundMaxTestCase,
reserveAmount btcutil.Amount) {
ht.FundCoins(testCase.initialWalletBalance, alice)
defer func() {
if testCase.initialWalletBalance <= 2_000 {
// Add additional funds to sweep "dust" UTXO.
ht.FundCoins(100_000, alice)
}
// Remove all funds from Alice.
sweepNodeWalletAndAssert(ht, alice)
}()
commitType := testCase.commitmentType
if commitType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
commitType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
}
// The parameters to try opening the channel with.
chanParams := lntest.OpenChannelParams{
Amt: 0,
PushAmt: testCase.pushAmt,
SatPerVByte: testCase.feeRate,
CommitmentType: commitType,
FundMax: true,
Private: testCase.private,
}
// If we don't expect the channel opening to be
// successful, simply check for an error.
if testCase.chanOpenShouldFail {
expectedErr := fmt.Errorf(testCase.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)
// Close the channel between Alice and Bob, asserting
// that the channel has been properly closed on-chain.
defer ht.CloseChannel(alice, chanPoint)
cType := ht.GetChannelCommitType(alice, chanPoint)
// Alice's balance should be her amount subtracted by the commitment
// transaction fee.
checkChannelBalance(
ht, alice,
testCase.expectedBalanceAlice-lntest.CalcStaticFee(cType, 0),
testCase.pushAmt,
)
// Ensure Bob's balance within the channel is equal to the push amount.
checkChannelBalance(
ht, bob, testCase.pushAmt,
testCase.expectedBalanceAlice-lntest.CalcStaticFee(cType, 0),
)
if lntest.CommitTypeHasAnchors(testCase.commitmentType) &&
!testCase.private {
ht.AssertWalletAccountBalance(
alice, lnwallet.DefaultAccountName,
int64(reserveAmount), 0,
)
}
}
// Creates a helper closure to be used below which asserts the proper
// response to a channel balance RPC.
func checkChannelBalance(ht *lntest.HarnessTest, node *node.HarnessNode,
local, remote btcutil.Amount) {
expectedResponse := &lnrpc.ChannelBalanceResponse{
LocalBalance: &lnrpc.Amount{
Sat: uint64(local),
Msat: uint64(lnwire.NewMSatFromSatoshis(local)),
},
RemoteBalance: &lnrpc.Amount{
Sat: uint64(remote),
Msat: uint64(lnwire.NewMSatFromSatoshis(
remote,
)),
},
UnsettledLocalBalance: &lnrpc.Amount{},
UnsettledRemoteBalance: &lnrpc.Amount{},
PendingOpenLocalBalance: &lnrpc.Amount{},
PendingOpenRemoteBalance: &lnrpc.Amount{},
// Deprecated fields.
Balance: int64(local),
}
ht.AssertChannelBalanceResp(node, expectedResponse)
}
// fundingFee returns the fee estimate used for a tx with the given number of
// inputs and the optional change output. This matches the estimate done by the
// wallet.
func fundingFee(numInput int, change bool) btcutil.Amount {
var weightEstimate input.TxWeightEstimator
// The standard fee rate used for a funding transaction.
var feeRate = chainfee.SatPerKWeight(12500)
// All inputs.
for i := 0; i < numInput; i++ {
weightEstimate.AddP2WKHInput()
}
// The multisig funding output.
weightEstimate.AddP2WSHOutput()
// Optionally count a change output.
if change {
weightEstimate.AddP2TROutput()
}
totalWeight := int64(weightEstimate.Weight())
return feeRate.FeeForWeight(totalWeight)
}
// sweepNodeWalletAndAssert sweeps funds from a node wallet.
func sweepNodeWalletAndAssert(ht *lntest.HarnessTest, node *node.HarnessNode) {
// New miner address we will sweep all funds to.
minerAddr, err := ht.Miner.NewAddress()
require.NoError(ht, err)
// Send all funds back to the miner node.
node.RPC.SendCoins(&lnrpc.SendCoinsRequest{
Addr: minerAddr.String(),
SendAll: true,
})
// Ensures we don't leave any transaction in the mempool after sweeping.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Ensure that the node's balance is 0
checkChannelBalance(ht, node, 0, 0)
}
+3
View File
@@ -6,6 +6,9 @@ s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2} [[:digit:]]{2}:[[:digit:]]{2}:[[:
s/[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}T[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\.[[:digit:]]{1,18}Z/<time>/g
s/[[:digit:]]+\:[[:digit:]]+\:[[:digit:]]+/<chan>/g
s/[[:digit:]]+ mSAT/<amt>/g
s/\-?[[:digit:]]+(\.[[:digit:]]{0,8})? BTC/<amt>/g
s/([fee|minimum])=[[:digit:]]+/\1=<amt>/g
s/funder balance too small \(\-?[[:digit:]]+\)/funder balance too small (<amt>)/g
s/HTLC ID = [[:digit:]]+/HTLC ID = <id>/g
s/height=[[:digit:]]+/height=<height>/g
s/collecting result for shard [[:digit:]]+/collecting result for shard <number>/g
+1944 -1932
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -2267,6 +2267,13 @@ message OpenChannelRequest {
capacity.
*/
uint64 remote_chan_reserve_sat = 25;
/*
If set, then lnd will attempt to commit all the coins under control of the
internal wallet to open the channel, and the LocalFundingAmount field must
be zero and is ignored.
*/
bool fund_max = 26;
}
message OpenStatusUpdate {
oneof update {
+4
View File
@@ -5816,6 +5816,10 @@
"type": "string",
"format": "uint64",
"description": "The number of satoshis we require the remote peer to reserve. This value,\nif specified, must be above the dust limit and below 20% of the channel\ncapacity."
},
"fund_max": {
"type": "boolean",
"description": "If set, then lnd will attempt to commit all the coins under control of the\ninternal wallet to open the channel, and the LocalFundingAmount field must\nbe zero and is ignored."
}
}
},
+5
View File
@@ -886,6 +886,10 @@ type OpenChannelParams struct {
// If set to false it avoids applying a fee rate of 0 and instead
// activates the default configured fee rate.
UseFeeRate bool
// FundMax is a boolean indicating whether the channel should be funded
// with the maximum possible amount from the wallet.
FundMax bool
}
// prepareOpenChannel waits for both nodes to be synced to chain and returns an
@@ -926,6 +930,7 @@ func (h *HarnessTest) prepareOpenChannel(srcNode, destNode *node.HarnessNode,
FeeRate: p.FeeRate,
UseBaseFee: p.UseBaseFee,
UseFeeRate: p.UseFeeRate,
FundMax: p.FundMax,
}
}
+26 -1
View File
@@ -52,13 +52,38 @@ type OutpointLocker interface {
// Assembler.
type Request struct {
// LocalAmt is the amount of coins we're placing into the funding
// output.
// output. LocalAmt must not be set if FundUpToMaxAmt is set.
LocalAmt btcutil.Amount
// RemoteAmt is the amount of coins the remote party is contributing to
// the funding output.
RemoteAmt btcutil.Amount
// FundUpToMaxAmt should be set to a non-zero amount if the channel
// funding should try to add as many funds to LocalAmt as possible
// until at most this amount is reached.
FundUpToMaxAmt btcutil.Amount
// MinFundAmt should be set iff the FundUpToMaxAmt field is set. It
// either carries the configured minimum channel capacity or, if an
// initial remote balance is specified, enough to cover the initial
// remote balance.
MinFundAmt btcutil.Amount
// RemoteChanReserve is the channel reserve we required for the remote
// peer.
RemoteChanReserve btcutil.Amount
// PushAmt is the number of satoshis that should be pushed over the
// responder as part of the initial channel creation.
PushAmt btcutil.Amount
// WalletReserve is a reserved amount that is not used to fund the
// channel when a maximum amount defined by FundUpToMaxAmt is set. This
// is useful when a reserved wallet balance must stay available due to
// e.g. anchor channels.
WalletReserve 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.
+10 -2
View File
@@ -195,13 +195,21 @@ func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint,
//
// NOTE: This method satisfies the chanfunding.Assembler interface.
func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
// We'll exit out if this field is set as the funding transaction has
// already been assembled, so we don't influence coin selection..
// We'll exit out if SubtractFees is set as the funding transaction has
// already been assembled, so we don't influence coin selection.
if req.SubtractFees {
return nil, fmt.Errorf("SubtractFees ignored, funding " +
"transaction is frozen")
}
// We'll exit out if FundUpToMaxAmt or MinFundAmt is set as the funding
// transaction has already been assembled, so we don't influence coin
// selection.
if req.FundUpToMaxAmt != 0 || req.MinFundAmt != 0 {
return nil, fmt.Errorf("FundUpToMaxAmt and MinFundAmt " +
"ignored, funding transaction is frozen")
}
intent := &ShimIntent{
localKey: c.localKey,
remoteKey: c.remoteKey,
+102
View File
@@ -1,6 +1,7 @@
package chanfunding
import (
"errors"
"fmt"
"github.com/btcsuite/btcd/btcutil"
@@ -53,6 +54,7 @@ type Coin struct {
// selected coins are returned in order for the caller to properly handle
// change+fees.
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
satSelected := btcutil.Amount(0)
for i, coin := range coins {
satSelected += btcutil.Amount(coin.Value)
@@ -251,3 +253,103 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
return selectedUtxos, outputAmt, changeAmt, nil
}
// CoinSelectUpToAmount attempts to select coins such that we'll select up to
// maxAmount exclusive of fees and optional reserve if sufficient funds are
// available. If insufficient funds are available this method selects all
// available coins.
func CoinSelectUpToAmount(feeRate chainfee.SatPerKWeight, minAmount, maxAmount,
reserved, dustLimit btcutil.Amount, coins []Coin) ([]Coin,
btcutil.Amount, btcutil.Amount, error) {
var (
// selectSubtractFee is tracking if our coin selection was
// unsuccessful and whether we have to start a new round of
// selecting coins considering fees.
selectSubtractFee = false
outputAmount = maxAmount
)
// Get total balance from coins which we need for reserve considerations
// and fee santiy checks.
var totalBalance btcutil.Amount
for _, coin := range coins {
totalBalance += btcutil.Amount(coin.Value)
}
// First we try to select coins to create an output of the specified
// maxAmount with or without a change output that covers the miner fee.
selected, changeAmt, err := CoinSelect(
feeRate, maxAmount, dustLimit, coins,
)
var errInsufficientFunds *ErrInsufficientFunds
if err == nil { //nolint:gocritic,ifElseChain
// If the coin selection succeeds we check if our total balance
// covers the selected set of coins including fees plus an
// optional anchor reserve.
// First we sum up the value of all selected coins.
var sumSelected btcutil.Amount
for _, coin := range selected {
sumSelected += btcutil.Amount(coin.Value)
}
// We then subtract the change amount from the value of all
// selected coins to obtain the actual amount that is selected.
sumSelected -= changeAmt
// Next we check if our total balance can cover for the selected
// output plus the optional anchor reserve.
if totalBalance-sumSelected < reserved {
// If our local balance is insufficient to cover for the
// reserve we try to select an output amount that uses
// our total balance minus reserve and fees.
selectSubtractFee = true
}
} else if errors.As(err, &errInsufficientFunds) {
// If the initial coin selection fails due to insufficient funds
// we select our total available balance minus fees.
selectSubtractFee = true
} else {
return nil, 0, 0, err
}
// If we determined that our local balance is insufficient we check
// our total balance minus fees and optional reserve.
if selectSubtractFee {
selected, outputAmount, changeAmt, err = CoinSelectSubtractFees(
feeRate, totalBalance-reserved, dustLimit, coins,
)
if err != nil {
return nil, 0, 0, err
}
}
// Sanity check the resulting output values to make sure we don't burn a
// great part to fees.
totalOut := outputAmount + changeAmt
sum := func(coins []Coin) btcutil.Amount {
var sum btcutil.Amount
for _, coin := range coins {
sum += btcutil.Amount(coin.Value)
}
return sum
}
err = sanityCheckFee(totalOut, sum(selected)-totalOut)
if err != nil {
return nil, 0, 0, err
}
// In case the selected amount is lower than minimum funding amount we
// must return an error. The minimum funding amount is determined
// upstream and denotes either the minimum viable channel size or an
// amount sufficient to cover for the initial remote balance.
if outputAmount < minAmount {
return nil, 0, 0, fmt.Errorf("available funds(%v) below the "+
"minimum amount(%v)", outputAmount, minAmount)
}
return selected, outputAmount, changeAmt, nil
}
+226 -1
View File
@@ -518,15 +518,240 @@ func TestCoinSelectSubtractFees(t *testing.T) {
}
}
// Assert we got the expected change amount.
// Assert we got the expected funding amount.
if localFundingAmt != test.expectedFundingAmt {
t.Fatalf("expected %v local funding amt, got %v",
test.expectedFundingAmt, localFundingAmt)
}
// Assert we got the expected change amount.
require.EqualValues(
t, test.expectedChange, changeAmt,
)
})
}
}
// TestCoinSelectUpToAmount tests that we pick coins adding up to the
// expected amount when creating a funding transaction, and that a change
// output is created only when necessary.
func TestCoinSelectUpToAmount(t *testing.T) {
t.Parallel()
const (
feeRate = chainfee.SatPerKWeight(100)
dustLimit = btcutil.Amount(1000)
dust = btcutil.Amount(100)
coin = btcutil.SatoshiPerBitcoin
minValue = 20_000
)
type testCase struct {
name string
minValue btcutil.Amount
maxValue btcutil.Amount
reserved btcutil.Amount
coins []Coin
expectedInput []btcutil.Amount
expectedFundingAmt btcutil.Amount
expectedChange btcutil.Amount
expectErr string
}
testCases := []testCase{{
// We have 1.0 BTC available, spend them all.
// This should lead to a funding TX with one output, the rest
// goes to fees.
name: "spend exactly all",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 1 * coin,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{1 * coin},
expectedFundingAmt: 1*coin - fundingFee(feeRate, 1, false),
expectedChange: 0,
}, {
// We have 1.0 BTC available and want to spend up to 2 BTC.
// This should lead to a funding TX with one output, the rest
// goes to fees.
name: "spend more",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 2 * coin,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{1 * coin},
expectedFundingAmt: 1*coin - fundingFee(feeRate, 1, false),
expectedChange: 0,
}, {
// We have 1.0 BTC available and want to spend up to 0.5 BTC.
// This should lead to a funding TX with one output and a
// change to subtract the fees from.
name: "spend far below",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 0.5 * coin,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{1 * coin},
expectedFundingAmt: 0.5 * coin,
expectedChange: 0.5*coin - fundingFee(feeRate, 1, true),
}, {
// We have 1.0 BTC available and want to spend just 1 Satoshi
// below that amount.
// This should lead to a funding TX with one output where the
// fee is subtracted from the total 1 BTC input value.
name: "spend little below",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 1*coin - 1,
// The one and only input will be selected.
expectedInput: []btcutil.Amount{
1 * coin,
},
expectedFundingAmt: 1*coin - fundingFee(feeRate, 1, false),
expectedChange: 0,
}, {
// The total funds available is below the dust limit after
// paying fees.
name: "dust output",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: int64(
fundingFee(feeRate, 1, false) + dust,
),
},
}},
minValue: minValue,
maxValue: fundingFee(feeRate, 1, false) + dust,
expectErr: "output amount(0.000001 BTC) after subtracting " +
"fees(0.00000048 BTC) below dust limit(0.00001 BTC)",
}, {
// If more than 20% of available wallet funds goes to fees, it
// should fail.
name: "high fee",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: int64(
20 * fundingFee(feeRate, 1, false),
),
},
}},
minValue: minValue,
maxValue: 16 * fundingFee(feeRate, 1, false),
expectErr: "fee 0.00000192 BTC on total output value " +
"0.00000768 BTC",
}, {
// This test makes sure that the implementation detail of using
// CoinSelect and CoinSelectSubtractFees is done correctly.
// CoinSelect always defaults to use a fee for single input -
// one change tx, whereas CoinSelectSubtractFees will use a fee
// of single input - no change output, which without a sanity
// check could result in a local amount higher than the maximum
// amount that was expected.
name: "sanity check for correct maximum amount",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 1*coin - fundingFee(feeRate, 1, false) - 1,
expectedInput: []btcutil.Amount{1 * coin},
expectedFundingAmt: 1*coin - fundingFee(feeRate, 1, false) - 1,
expectedChange: 0,
}, {
// This test makes sure that if a reserved value is required
// then it is handled correctly by leaving exactly the reserved
// value as change and still maxing out the funding amount.
name: "sanity check for correct reserved amount subtract " +
"from total",
coins: []Coin{{
TxOut: wire.TxOut{
PkScript: p2wkhScript,
Value: 1 * coin,
},
}},
minValue: minValue,
maxValue: 1*coin - 9000,
reserved: 10000,
expectedInput: []btcutil.Amount{1 * coin},
expectedFundingAmt: 1*coin -
fundingFee(feeRate, 1, true) - 10000,
expectedChange: 10000,
}}
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
selected, localFundingAmt, changeAmt,
err := CoinSelectUpToAmount(
feeRate, test.minValue, test.maxValue,
test.reserved, dustLimit, test.coins,
)
if len(test.expectErr) == 0 && err != nil {
t.Fatalf(err.Error())
}
if changeAmt != test.expectedChange {
t.Fatalf("expected %v change amt, got %v",
test.expectedChange, changeAmt)
}
if len(test.expectErr) > 0 && err == nil {
t.Fatalf("expected error: %v", test.expectErr)
}
if len(test.expectErr) > 0 && err != nil {
require.EqualError(t, err, test.expectErr)
}
// Check that the selected inputs match what we expect.
require.Equal(t, len(test.expectedInput), len(selected))
for i, coin := range selected {
require.EqualValues(
t, test.expectedInput[i], coin.Value,
)
}
// Assert we got the expected funding amount.
require.Equal(
t, test.expectedFundingAmt, localFundingAmt,
)
// Assert we got the expected change amount.
require.Equal(
t, test.expectedChange, changeAmt,
)
})
}
}
+9 -1
View File
@@ -514,12 +514,20 @@ func NewPsbtAssembler(fundingAmt btcutil.Amount, basePsbt *psbt.Packet,
//
// NOTE: This method satisfies the chanfunding.Assembler interface.
func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) {
// We'll exit out if this field is set as the funding transaction will
// We'll exit out if SubtractFees is set as the funding transaction will
// be assembled externally, so we don't influence coin selection.
if req.SubtractFees {
return nil, fmt.Errorf("SubtractFees not supported for PSBT")
}
// We'll exit out if FundUpToMaxAmt or MinFundAmt is set as the funding
// transaction will be assembled externally, so we don't influence coin
// selection.
if req.FundUpToMaxAmt != 0 || req.MinFundAmt != 0 {
return nil, fmt.Errorf("FundUpToMaxAmt and MinFundAmt not " +
"supported for PSBT")
}
intent := &PsbtIntent{
ShimIntent: ShimIntent{
localFundingAmt: p.fundingAmt,
+49 -2
View File
@@ -284,16 +284,63 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// 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:
case r.LocalAmt == 0 && r.FundUpToMaxAmt == 0:
break
// The local funding amount cannot be used in combination with
// the funding up to some maximum amount. If that is the case
// we return an error.
case r.LocalAmt != 0 && r.FundUpToMaxAmt != 0:
return fmt.Errorf("cannot use a local funding amount " +
"and fundmax parameters")
// We cannot use the subtract fees flag while using the funding
// up to some maximum amount. If that is the case we return an
// error.
case r.SubtractFees && r.FundUpToMaxAmt != 0:
return fmt.Errorf("cannot subtract fees from local " +
"amount while using fundmax parameters")
// In case this request uses funding up to some maximum amount,
// we will call the specialized coin selection function for
// that.
case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0:
selectedCoins, localContributionAmt, changeAmt,
err = CoinSelectUpToAmount(
r.FeeRate, r.MinFundAmt, r.FundUpToMaxAmt,
r.WalletReserve, w.cfg.DustLimit, coins,
)
if err != nil {
return err
}
// Now where the actual channel capacity is determined
// we can check for local contribution constraints.
//
// Ensure that the remote channel reserve does not
// exceed 20% of the channel capacity.
if r.RemoteChanReserve >= localContributionAmt/5 {
return fmt.Errorf("remote channel reserve " +
"must be less than the %%20 of the " +
"channel capacity")
}
// Ensure that the initial remote balance does not
// exceed our local contribution as that would leave a
// negative balance on our side.
if r.PushAmt >= localContributionAmt {
return fmt.Errorf("amount pushed to remote " +
"peer for initial state must be " +
"below the local funding amount")
}
// 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(
selectedCoins, localContributionAmt, changeAmt,
err = CoinSelectSubtractFees(
r.FeeRate, r.LocalAmt, dustLimit, coins,
)
if err != nil {
+57 -9
View File
@@ -121,6 +121,20 @@ type InitFundingReserveMsg struct {
// to this channel.
RemoteFundingAmt btcutil.Amount
// FundUpToMaxAmt defines if channel funding should try to add as many
// funds to the channel opening as possible up to this amount. If used,
// then MinFundAmt is treated as the minimum amount of funds that must
// be available to open the channel. If set to zero it is ignored.
FundUpToMaxAmt btcutil.Amount
// MinFundAmt denotes the minimum channel capacity that has to be
// allocated iff the FundUpToMaxAmt is set.
MinFundAmt btcutil.Amount
// RemoteChanReserve is the channel reserve we required for the remote
// peer.
RemoteChanReserve btcutil.Amount
// CommitFeePerKw is the starting accepted satoshis/Kw fee for the set
// of initial commitment transactions. In order to ensure timely
// confirmation, it is recommended that this fee should be generous,
@@ -772,8 +786,12 @@ func (l *LightningWallet) CancelFundingIntent(pid [32]byte) error {
// handleFundingReserveRequest processes a message intending to create, and
// validate a funding reservation request.
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
noFundsCommitted := req.LocalFundingAmt == 0 &&
req.RemoteFundingAmt == 0 && req.FundUpToMaxAmt == 0
// It isn't possible to create a channel with zero funds committed.
if req.LocalFundingAmt+req.RemoteFundingAmt == 0 {
if noFundsCommitted {
err := ErrZeroCapacity()
req.err <- err
req.resp <- nil
@@ -818,6 +836,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
localFundingAmt := req.LocalFundingAmt
remoteFundingAmt := req.RemoteFundingAmt
hasAnchors := req.CommitType.HasAnchors()
var (
fundingIntent chanfunding.Intent
@@ -837,12 +856,45 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
// funder in the attached request to provision the inputs/outputs
// that'll ultimately be used to construct the funding transaction.
if !ok {
var err error
var numAnchorChans int
// Get the number of anchor channels to determine if there is a
// reserved value that must be respected when funding up to the
// maximum amount. Since private channels (most likely) won't be
// used for routing other than the last hop, they bear a smaller
// risk that we must force close them in order to resolve a HTLC
// up/downstream. Hence we exclude them from the count of anchor
// channels in order to attribute the respective anchor amount
// to the channel capacity.
if req.FundUpToMaxAmt > 0 && req.MinFundAmt > 0 {
numAnchorChans, err = l.CurrentNumAnchorChans()
if err != nil {
req.err <- err
req.resp <- nil
return
}
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
if hasAnchors && isPublic {
numAnchorChans++
}
}
// Coin selection is done on the basis of sat/kw, so we'll use
// the fee rate passed in to perform coin selection.
var err error
fundingReq := &chanfunding.Request{
RemoteAmt: req.RemoteFundingAmt,
LocalAmt: req.LocalFundingAmt,
RemoteAmt: req.RemoteFundingAmt,
LocalAmt: req.LocalFundingAmt,
FundUpToMaxAmt: req.FundUpToMaxAmt,
MinFundAmt: req.MinFundAmt,
RemoteChanReserve: req.RemoteChanReserve,
PushAmt: lnwire.MilliSatoshi.ToSatoshis(
req.PushMSat,
),
WalletReserve: l.RequiredReserve(
uint32(numAnchorChans),
),
MinConfs: req.MinConfs,
SubtractFees: req.SubtractFees,
FeeRate: req.FundingFeePerKw,
@@ -915,7 +967,6 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
// funding tx ready, so this will always pass. We'll do another check
// when the PSBT has been verified.
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
hasAnchors := req.CommitType.HasAnchors()
if enforceNewReservedValue {
err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors)
if err != nil {
@@ -1136,10 +1187,7 @@ func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint,
}
// We reserve a given amount for each anchor channel.
reserved := btcutil.Amount(numAnchorChans) * AnchorChanReservedValue
if reserved > MaxAnchorChanReservedValue {
reserved = MaxAnchorChanReservedValue
}
reserved := l.RequiredReserve(uint32(numAnchorChans))
if walletBalance < reserved {
walletLog.Debugf("Reserved value=%v above final "+
+78 -25
View File
@@ -1934,6 +1934,62 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
localFundingAmt := btcutil.Amount(in.LocalFundingAmount)
remoteInitialBalance := btcutil.Amount(in.PushSat)
// If we are not committing the maximum viable balance towards a channel
// then the local funding amount must be specified. In case FundMax is
// set the funding amount is specified as the interval between minimum
// funding amount and by the configured maximum channel size.
if !in.FundMax && localFundingAmt == 0 {
return nil, fmt.Errorf("local funding amount must be non-zero")
}
// Ensure that the initial balance of the remote party (if pushing
// satoshis) does not exceed the amount the local party has requested
// for funding. This is only checked if we are not committing the
// maximum viable amount towards the channel balance. If we do commit
// the maximum then the remote balance is checked in a dedicated FundMax
// check.
if !in.FundMax && remoteInitialBalance >= localFundingAmt {
return nil, fmt.Errorf("amount pushed to remote peer for " +
"initial state must be below the local funding amount")
}
// We either allow the fundmax or the psbt flow hence we return an error
// if both are set.
if in.FundingShim != nil && in.FundMax {
return nil, fmt.Errorf("cannot provide a psbt funding shim " +
"while committing the maxium wallet balance towards " +
"the channel opening")
}
// If the FundMax flag is set, ensure that the acceptable minimum local
// amount adheres to the amount to be pushed to the remote, and to
// current rules, while also respecting the settings for the maximum
// channel size.
var minFundAmt, fundUpToMaxAmt btcutil.Amount
if in.FundMax {
// We assume the configured maximum channel size to be the upper
// bound of our "maxed" out funding attempt.
fundUpToMaxAmt = btcutil.Amount(r.cfg.MaxChanSize)
// Since the standard non-fundmax flow requires the minimum
// funding amount to be at least in the amount of the initial
// remote balance(push amount) we need to adjust the minimum
// funding amount accordingly. We initially assume the minimum
// allowed channel size as minimum funding amount.
minFundAmt = funding.MinChanFundingSize
// If minFundAmt is less than the initial remote balance we
// simply assign the initial remote balance to minFundAmt in
// order to fullfil the criterion. Whether or not this so
// determined minimum amount is actually available is
// ascertained downstream in the lnwallet's reservation
// workflow.
if remoteInitialBalance >= minFundAmt {
minFundAmt = remoteInitialBalance
}
}
minHtlcIn := lnwire.MilliSatoshi(in.MinHtlcMsat)
remoteCsvDelay := uint16(in.RemoteCsvDelay)
maxValue := lnwire.MilliSatoshi(in.RemoteMaxValueInFlightMsat)
@@ -1942,20 +1998,6 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
globalFeatureSet := r.server.featureMgr.Get(feature.SetNodeAnn)
// Ensure that a local funding amount has been specified.
if localFundingAmt == 0 {
return nil, fmt.Errorf("local funding amount must be non-zero")
}
// Ensure that the initial balance of the remote party (if pushing
// satoshis) does not exceed the amount the local party has requested
// for funding.
//
if remoteInitialBalance >= localFundingAmt {
return nil, fmt.Errorf("amount pushed to remote peer for " +
"initial state must be below the local funding amount")
}
// Determine if the user provided channel fees
// and if so pass them on to the funding workflow.
var channelBaseFee, channelFeeRate *uint64
@@ -1968,7 +2010,7 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
// Ensure that the remote channel reserve does not exceed 20% of the
// channel capacity.
if remoteChanReserve >= localFundingAmt/5 {
if !in.FundMax && remoteChanReserve >= localFundingAmt/5 {
return nil, fmt.Errorf("remote channel reserve must be less " +
"than the %%20 of the channel capacity")
}
@@ -1976,18 +2018,25 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
// Ensure that the user doesn't exceed the current soft-limit for
// channel size. If the funding amount is above the soft-limit, then
// we'll reject the request.
// If the FundMax flag is set the local amount is determined downstream
// in the wallet hence we do not check it here against the maximum
// funding amount. Only if the localFundingAmt is specified we can check
// if it exceeds the maximum funding amount.
wumboEnabled := globalFeatureSet.HasFeature(
lnwire.WumboChannelsOptional,
)
if !wumboEnabled && localFundingAmt > MaxFundingAmount {
if !in.FundMax && !wumboEnabled && localFundingAmt > MaxFundingAmount {
return nil, fmt.Errorf("funding amount is too large, the max "+
"channel size is: %v", MaxFundingAmount)
}
// Restrict the size of the channel we'll actually open. At a later
// level, we'll ensure that the output we create after accounting for
// fees that a dust output isn't created.
if localFundingAmt < funding.MinChanFundingSize {
// level, we'll ensure that the output we create, after accounting for
// fees, does not leave a dust output. In case of the FundMax flow
// dedicated checks ensure that the lower boundary of the channel size
// is at least in the amount of MinChanFundingSize or potentially higher
// if a remote balance is specified.
if !in.FundMax && localFundingAmt < funding.MinChanFundingSize {
return nil, fmt.Errorf("channel is too small, the minimum "+
"channel size is: %v SAT", int64(funding.MinChanFundingSize))
}
@@ -2128,12 +2177,14 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
// 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.
return &funding.InitFundingMsg{
TargetPubkey: nodePubKey,
ChainHash: *r.cfg.ActiveNetParams.GenesisHash,
LocalFundingAmt: localFundingAmt,
BaseFee: channelBaseFee,
FeeRate: channelFeeRate,
PushAmt: lnwire.NewMSatFromSatoshis(remoteInitialBalance),
TargetPubkey: nodePubKey,
ChainHash: *r.cfg.ActiveNetParams.GenesisHash,
LocalFundingAmt: localFundingAmt,
BaseFee: channelBaseFee,
FeeRate: channelFeeRate,
PushAmt: lnwire.NewMSatFromSatoshis(
remoteInitialBalance,
),
MinHtlcIn: minHtlcIn,
FundingFeePerKw: feeRate,
Private: in.Private,
@@ -2145,6 +2196,8 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
MaxHtlcs: maxHtlcs,
MaxLocalCsv: uint16(in.MaxLocalCsv),
ChannelType: channelType,
FundUpToMaxAmt: fundUpToMaxAmt,
MinFundAmt: minFundAmt,
}, nil
}