Merge pull request #6903 from hieblmi/fundmax-flag
funding: add `fundmax` flag to `OpenChannelRequest`
This commit is contained in:
19 files changed
+3110
-1997
No files matched your search
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,4 +519,8 @@ var allTestCases = []*lntest.TestCase{
|
||||
Name: "watchtower session management",
|
||||
TestFunc: testWatchtowerSessionManagement,
|
||||
},
|
||||
{
|
||||
Name: "channel fundmax",
|
||||
TestFunc: testChannelFundMax,
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user