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 " +
|
Usage: "the number of satoshis the wallet should " +
|
||||||
"commit to the channel",
|
"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{
|
cli.Uint64Flag{
|
||||||
Name: "base_fee_msat",
|
Name: "base_fee_msat",
|
||||||
Usage: "the base fee in milli-satoshis that will " +
|
Usage: "the base fee in milli-satoshis that will " +
|
||||||
@@ -292,6 +299,7 @@ func openChannel(ctx *cli.Context) error {
|
|||||||
ZeroConf: ctx.Bool("zero_conf"),
|
ZeroConf: ctx.Bool("zero_conf"),
|
||||||
ScidAlias: ctx.Bool("scid_alias"),
|
ScidAlias: ctx.Bool("scid_alias"),
|
||||||
RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"),
|
RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"),
|
||||||
|
FundMax: ctx.Bool("fundmax"),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -350,8 +358,23 @@ func openChannel(ctx *cli.Context) error {
|
|||||||
return fmt.Errorf("unable to decode local amt: %v", err)
|
return fmt.Errorf("unable to decode local amt: %v", err)
|
||||||
}
|
}
|
||||||
args = args.Tail()
|
args = args.Tail()
|
||||||
default:
|
case !ctx.Bool("fundmax"):
|
||||||
return fmt.Errorf("local amt argument missing")
|
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") {
|
if ctx.IsSet("push_amt") {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
restore single channels](https://github.com/lightningnetwork/lnd/pull/7437)
|
restore single channels](https://github.com/lightningnetwork/lnd/pull/7437)
|
||||||
to and from a file on disk.
|
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
|
## Watchtowers
|
||||||
|
|
||||||
* [Fix Address iterator
|
* [Fix Address iterator
|
||||||
@@ -66,6 +70,7 @@ available](https://github.com/lightningnetwork/lnd/pull/7529).
|
|||||||
|
|
||||||
* ardevd
|
* ardevd
|
||||||
* Elle Mouton
|
* Elle Mouton
|
||||||
|
* hieblmi
|
||||||
* Oliver Gugger
|
* Oliver Gugger
|
||||||
* Tommy Volk
|
* Tommy Volk
|
||||||
* Yong Yu
|
* Yong Yu
|
||||||
|
|||||||
+29
-17
@@ -266,6 +266,15 @@ type InitFundingMsg struct {
|
|||||||
// peer.
|
// peer.
|
||||||
MaxLocalCsv uint16
|
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
|
// ChanFunder is an optional channel funder that allows the caller to
|
||||||
// control exactly how the channel funding is carried out. If not
|
// control exactly how the channel funding is carried out. If not
|
||||||
// specified, then the default chanfunding.WalletAssembler will be
|
// specified, then the default chanfunding.WalletAssembler will be
|
||||||
@@ -4079,23 +4088,26 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &lnwallet.InitFundingReserveMsg{
|
req := &lnwallet.InitFundingReserveMsg{
|
||||||
ChainHash: &msg.ChainHash,
|
ChainHash: &msg.ChainHash,
|
||||||
PendingChanID: chanID,
|
PendingChanID: chanID,
|
||||||
NodeID: peerKey,
|
NodeID: peerKey,
|
||||||
NodeAddr: msg.Peer.Address(),
|
NodeAddr: msg.Peer.Address(),
|
||||||
SubtractFees: msg.SubtractFees,
|
SubtractFees: msg.SubtractFees,
|
||||||
LocalFundingAmt: localAmt,
|
LocalFundingAmt: localAmt,
|
||||||
RemoteFundingAmt: 0,
|
RemoteFundingAmt: 0,
|
||||||
CommitFeePerKw: commitFeePerKw,
|
FundUpToMaxAmt: msg.FundUpToMaxAmt,
|
||||||
FundingFeePerKw: msg.FundingFeePerKw,
|
MinFundAmt: msg.MinFundAmt,
|
||||||
PushMSat: msg.PushAmt,
|
RemoteChanReserve: chanReserve,
|
||||||
Flags: channelFlags,
|
CommitFeePerKw: commitFeePerKw,
|
||||||
MinConfs: msg.MinConfs,
|
FundingFeePerKw: msg.FundingFeePerKw,
|
||||||
CommitType: commitType,
|
PushMSat: msg.PushAmt,
|
||||||
ChanFunder: msg.ChanFunder,
|
Flags: channelFlags,
|
||||||
ZeroConf: zeroConf,
|
MinConfs: msg.MinConfs,
|
||||||
OptionScidAlias: scid,
|
CommitType: commitType,
|
||||||
ScidAliasFeature: scidFeatureVal,
|
ChanFunder: msg.ChanFunder,
|
||||||
|
ZeroConf: zeroConf,
|
||||||
|
OptionScidAlias: scid,
|
||||||
|
ScidAliasFeature: scidFeatureVal,
|
||||||
}
|
}
|
||||||
|
|
||||||
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
|
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) {
|
*wire.OutPoint, *wire.MsgTx) {
|
||||||
|
|
||||||
publ := fundChannel(
|
publ := fundChannel(
|
||||||
t, alice, bob, localFundingAmt, pushAmt, false, numConfs,
|
t, alice, bob, localFundingAmt, pushAmt, false, 0, 0, numConfs,
|
||||||
updateChan, announceChan, nil,
|
updateChan, announceChan, nil,
|
||||||
)
|
)
|
||||||
fundingOutPoint := &wire.OutPoint{
|
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
|
// fundChannel takes the funding process to the point where the funding
|
||||||
// transaction is confirmed on-chain. Returns the funding tx.
|
// transaction is confirmed on-chain. Returns the funding tx.
|
||||||
func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
|
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,
|
updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool,
|
||||||
chanType *lnwire.ChannelType) *wire.MsgTx {
|
chanType *lnwire.ChannelType) *wire.MsgTx {
|
||||||
|
|
||||||
@@ -734,6 +735,8 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
|
|||||||
TargetPubkey: bob.privKey.PubKey(),
|
TargetPubkey: bob.privKey.PubKey(),
|
||||||
ChainHash: *fundingNetParams.GenesisHash,
|
ChainHash: *fundingNetParams.GenesisHash,
|
||||||
SubtractFees: subtractFees,
|
SubtractFees: subtractFees,
|
||||||
|
FundUpToMaxAmt: fundUpToMaxAmt,
|
||||||
|
MinFundAmt: minFundAmt,
|
||||||
LocalFundingAmt: localFundingAmt,
|
LocalFundingAmt: localFundingAmt,
|
||||||
PushAmt: lnwire.NewMSatFromSatoshis(pushAmt),
|
PushAmt: lnwire.NewMSatFromSatoshis(pushAmt),
|
||||||
FundingFeePerKw: 1000,
|
FundingFeePerKw: 1000,
|
||||||
@@ -3730,7 +3733,7 @@ func TestFundingManagerFundAll(t *testing.T) {
|
|||||||
// Initiate a fund channel, and inspect the funding tx.
|
// Initiate a fund channel, and inspect the funding tx.
|
||||||
pushAmt := btcutil.Amount(0)
|
pushAmt := btcutil.Amount(0)
|
||||||
fundingTx := fundChannel(
|
fundingTx := fundChannel(
|
||||||
t, alice, bob, test.spendAmt, pushAmt, true, 1,
|
t, alice, bob, test.spendAmt, pushAmt, true, 0, 0, 1,
|
||||||
updateChan, true, nil,
|
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
|
// TestGetUpfrontShutdown tests different combinations of inputs for getting a
|
||||||
// shutdown script. It varies whether the peer has the feature set, whether
|
// 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
|
// 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.
|
// Call fundChannel with the zero-conf ChannelType.
|
||||||
fundingTx := fundChannel(
|
fundingTx := fundChannel(
|
||||||
t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true,
|
t, alice, bob, fundingAmt, pushAmt, false, 0, 0, 1, updateChan,
|
||||||
&channelType,
|
true, &channelType,
|
||||||
)
|
)
|
||||||
fundingOp := &wire.OutPoint{
|
fundingOp := &wire.OutPoint{
|
||||||
Hash: fundingTx.TxHash(),
|
Hash: fundingTx.TxHash(),
|
||||||
@@ -4309,3 +4463,35 @@ func TestFundingManagerZeroConf(t *testing.T) {
|
|||||||
// have been deleted from the database, as the channel is announced.
|
// have been deleted from the database, as the channel is announced.
|
||||||
assertNoFwdingPolicy(t, alice, bob, fundingOp)
|
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",
|
Name: "watchtower session management",
|
||||||
TestFunc: testWatchtowerSessionManagement,
|
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:]]{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:]]+\:[[:digit:]]+\:[[:digit:]]+/<chan>/g
|
||||||
s/[[:digit:]]+ mSAT/<amt>/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/HTLC ID = [[:digit:]]+/HTLC ID = <id>/g
|
||||||
s/height=[[:digit:]]+/height=<height>/g
|
s/height=[[:digit:]]+/height=<height>/g
|
||||||
s/collecting result for shard [[:digit:]]+/collecting result for shard <number>/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.
|
capacity.
|
||||||
*/
|
*/
|
||||||
uint64 remote_chan_reserve_sat = 25;
|
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 {
|
message OpenStatusUpdate {
|
||||||
oneof update {
|
oneof update {
|
||||||
|
|||||||
@@ -5816,6 +5816,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uint64",
|
"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."
|
"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
|
// If set to false it avoids applying a fee rate of 0 and instead
|
||||||
// activates the default configured fee rate.
|
// activates the default configured fee rate.
|
||||||
UseFeeRate bool
|
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
|
// 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,
|
FeeRate: p.FeeRate,
|
||||||
UseBaseFee: p.UseBaseFee,
|
UseBaseFee: p.UseBaseFee,
|
||||||
UseFeeRate: p.UseFeeRate,
|
UseFeeRate: p.UseFeeRate,
|
||||||
|
FundMax: p.FundMax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,13 +52,38 @@ type OutpointLocker interface {
|
|||||||
// Assembler.
|
// Assembler.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// LocalAmt is the amount of coins we're placing into the funding
|
// 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
|
LocalAmt btcutil.Amount
|
||||||
|
|
||||||
// RemoteAmt is the amount of coins the remote party is contributing to
|
// RemoteAmt is the amount of coins the remote party is contributing to
|
||||||
// the funding output.
|
// the funding output.
|
||||||
RemoteAmt btcutil.Amount
|
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
|
// MinConfs controls how many confirmations a coin need to be eligible
|
||||||
// to be used as an input to the funding transaction. If this value is
|
// to be used as an input to the funding transaction. If this value is
|
||||||
// set to zero, then zero conf outputs may be spent.
|
// set to zero, then zero conf outputs may be spent.
|
||||||
|
|||||||
@@ -195,13 +195,21 @@ func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint,
|
|||||||
//
|
//
|
||||||
// NOTE: This method satisfies the chanfunding.Assembler interface.
|
// NOTE: This method satisfies the chanfunding.Assembler interface.
|
||||||
func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
|
func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
|
||||||
// We'll exit out if this field is set as the funding transaction has
|
// We'll exit out if SubtractFees is set as the funding transaction has
|
||||||
// already been assembled, so we don't influence coin selection..
|
// already been assembled, so we don't influence coin selection.
|
||||||
if req.SubtractFees {
|
if req.SubtractFees {
|
||||||
return nil, fmt.Errorf("SubtractFees ignored, funding " +
|
return nil, fmt.Errorf("SubtractFees ignored, funding " +
|
||||||
"transaction is frozen")
|
"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{
|
intent := &ShimIntent{
|
||||||
localKey: c.localKey,
|
localKey: c.localKey,
|
||||||
remoteKey: c.remoteKey,
|
remoteKey: c.remoteKey,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package chanfunding
|
package chanfunding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
@@ -53,6 +54,7 @@ type Coin struct {
|
|||||||
// selected coins are returned in order for the caller to properly handle
|
// selected coins are returned in order for the caller to properly handle
|
||||||
// change+fees.
|
// change+fees.
|
||||||
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
|
func selectInputs(amt btcutil.Amount, coins []Coin) (btcutil.Amount, []Coin, error) {
|
||||||
|
|
||||||
satSelected := btcutil.Amount(0)
|
satSelected := btcutil.Amount(0)
|
||||||
for i, coin := range coins {
|
for i, coin := range coins {
|
||||||
satSelected += btcutil.Amount(coin.Value)
|
satSelected += btcutil.Amount(coin.Value)
|
||||||
@@ -251,3 +253,103 @@ func CoinSelectSubtractFees(feeRate chainfee.SatPerKWeight, amt,
|
|||||||
|
|
||||||
return selectedUtxos, outputAmt, changeAmt, nil
|
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 {
|
if localFundingAmt != test.expectedFundingAmt {
|
||||||
t.Fatalf("expected %v local funding amt, got %v",
|
t.Fatalf("expected %v local funding amt, got %v",
|
||||||
test.expectedFundingAmt, localFundingAmt)
|
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 {
|
if changeAmt != test.expectedChange {
|
||||||
t.Fatalf("expected %v change amt, got %v",
|
t.Fatalf("expected %v change amt, got %v",
|
||||||
test.expectedChange, changeAmt)
|
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.
|
// NOTE: This method satisfies the chanfunding.Assembler interface.
|
||||||
func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) {
|
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.
|
// be assembled externally, so we don't influence coin selection.
|
||||||
if req.SubtractFees {
|
if req.SubtractFees {
|
||||||
return nil, fmt.Errorf("SubtractFees not supported for PSBT")
|
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{
|
intent := &PsbtIntent{
|
||||||
ShimIntent: ShimIntent{
|
ShimIntent: ShimIntent{
|
||||||
localFundingAmt: p.fundingAmt,
|
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
|
// If there's no funding amount at all (receiving an inbound
|
||||||
// single funder request), then we don't need to perform any
|
// single funder request), then we don't need to perform any
|
||||||
// coin selection at all.
|
// coin selection at all.
|
||||||
case r.LocalAmt == 0:
|
case r.LocalAmt == 0 && r.FundUpToMaxAmt == 0:
|
||||||
break
|
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
|
// In case this request want the fees subtracted from the local
|
||||||
// amount, we'll call the specialized method for that. This
|
// amount, we'll call the specialized method for that. This
|
||||||
// ensures that we won't deduct more that the specified balance
|
// ensures that we won't deduct more that the specified balance
|
||||||
// from our wallet.
|
// from our wallet.
|
||||||
case r.SubtractFees:
|
case r.SubtractFees:
|
||||||
dustLimit := w.cfg.DustLimit
|
dustLimit := w.cfg.DustLimit
|
||||||
selectedCoins, localContributionAmt, changeAmt, err = CoinSelectSubtractFees(
|
selectedCoins, localContributionAmt, changeAmt,
|
||||||
|
err = CoinSelectSubtractFees(
|
||||||
r.FeeRate, r.LocalAmt, dustLimit, coins,
|
r.FeeRate, r.LocalAmt, dustLimit, coins,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+57
-9
@@ -121,6 +121,20 @@ type InitFundingReserveMsg struct {
|
|||||||
// to this channel.
|
// to this channel.
|
||||||
RemoteFundingAmt btcutil.Amount
|
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
|
// CommitFeePerKw is the starting accepted satoshis/Kw fee for the set
|
||||||
// of initial commitment transactions. In order to ensure timely
|
// of initial commitment transactions. In order to ensure timely
|
||||||
// confirmation, it is recommended that this fee should be generous,
|
// 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
|
// handleFundingReserveRequest processes a message intending to create, and
|
||||||
// validate a funding reservation request.
|
// validate a funding reservation request.
|
||||||
func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg) {
|
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.
|
// It isn't possible to create a channel with zero funds committed.
|
||||||
if req.LocalFundingAmt+req.RemoteFundingAmt == 0 {
|
if noFundsCommitted {
|
||||||
err := ErrZeroCapacity()
|
err := ErrZeroCapacity()
|
||||||
req.err <- err
|
req.err <- err
|
||||||
req.resp <- nil
|
req.resp <- nil
|
||||||
@@ -818,6 +836,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
|
|||||||
|
|
||||||
localFundingAmt := req.LocalFundingAmt
|
localFundingAmt := req.LocalFundingAmt
|
||||||
remoteFundingAmt := req.RemoteFundingAmt
|
remoteFundingAmt := req.RemoteFundingAmt
|
||||||
|
hasAnchors := req.CommitType.HasAnchors()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fundingIntent chanfunding.Intent
|
fundingIntent chanfunding.Intent
|
||||||
@@ -837,12 +856,45 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
|
|||||||
// funder in the attached request to provision the inputs/outputs
|
// funder in the attached request to provision the inputs/outputs
|
||||||
// that'll ultimately be used to construct the funding transaction.
|
// that'll ultimately be used to construct the funding transaction.
|
||||||
if !ok {
|
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
|
// Coin selection is done on the basis of sat/kw, so we'll use
|
||||||
// the fee rate passed in to perform coin selection.
|
// the fee rate passed in to perform coin selection.
|
||||||
var err error
|
|
||||||
fundingReq := &chanfunding.Request{
|
fundingReq := &chanfunding.Request{
|
||||||
RemoteAmt: req.RemoteFundingAmt,
|
RemoteAmt: req.RemoteFundingAmt,
|
||||||
LocalAmt: req.LocalFundingAmt,
|
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,
|
MinConfs: req.MinConfs,
|
||||||
SubtractFees: req.SubtractFees,
|
SubtractFees: req.SubtractFees,
|
||||||
FeeRate: req.FundingFeePerKw,
|
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
|
// funding tx ready, so this will always pass. We'll do another check
|
||||||
// when the PSBT has been verified.
|
// when the PSBT has been verified.
|
||||||
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
|
isPublic := req.Flags&lnwire.FFAnnounceChannel != 0
|
||||||
hasAnchors := req.CommitType.HasAnchors()
|
|
||||||
if enforceNewReservedValue {
|
if enforceNewReservedValue {
|
||||||
err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors)
|
err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1136,10 +1187,7 @@ func (l *LightningWallet) CheckReservedValue(in []wire.OutPoint,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We reserve a given amount for each anchor channel.
|
// We reserve a given amount for each anchor channel.
|
||||||
reserved := btcutil.Amount(numAnchorChans) * AnchorChanReservedValue
|
reserved := l.RequiredReserve(uint32(numAnchorChans))
|
||||||
if reserved > MaxAnchorChanReservedValue {
|
|
||||||
reserved = MaxAnchorChanReservedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
if walletBalance < reserved {
|
if walletBalance < reserved {
|
||||||
walletLog.Debugf("Reserved value=%v above final "+
|
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)
|
localFundingAmt := btcutil.Amount(in.LocalFundingAmount)
|
||||||
remoteInitialBalance := btcutil.Amount(in.PushSat)
|
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)
|
minHtlcIn := lnwire.MilliSatoshi(in.MinHtlcMsat)
|
||||||
remoteCsvDelay := uint16(in.RemoteCsvDelay)
|
remoteCsvDelay := uint16(in.RemoteCsvDelay)
|
||||||
maxValue := lnwire.MilliSatoshi(in.RemoteMaxValueInFlightMsat)
|
maxValue := lnwire.MilliSatoshi(in.RemoteMaxValueInFlightMsat)
|
||||||
@@ -1942,20 +1998,6 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
|
|||||||
|
|
||||||
globalFeatureSet := r.server.featureMgr.Get(feature.SetNodeAnn)
|
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
|
// Determine if the user provided channel fees
|
||||||
// and if so pass them on to the funding workflow.
|
// and if so pass them on to the funding workflow.
|
||||||
var channelBaseFee, channelFeeRate *uint64
|
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
|
// Ensure that the remote channel reserve does not exceed 20% of the
|
||||||
// channel capacity.
|
// channel capacity.
|
||||||
if remoteChanReserve >= localFundingAmt/5 {
|
if !in.FundMax && remoteChanReserve >= localFundingAmt/5 {
|
||||||
return nil, fmt.Errorf("remote channel reserve must be less " +
|
return nil, fmt.Errorf("remote channel reserve must be less " +
|
||||||
"than the %%20 of the channel capacity")
|
"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
|
// Ensure that the user doesn't exceed the current soft-limit for
|
||||||
// channel size. If the funding amount is above the soft-limit, then
|
// channel size. If the funding amount is above the soft-limit, then
|
||||||
// we'll reject the request.
|
// 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(
|
wumboEnabled := globalFeatureSet.HasFeature(
|
||||||
lnwire.WumboChannelsOptional,
|
lnwire.WumboChannelsOptional,
|
||||||
)
|
)
|
||||||
if !wumboEnabled && localFundingAmt > MaxFundingAmount {
|
if !in.FundMax && !wumboEnabled && localFundingAmt > MaxFundingAmount {
|
||||||
return nil, fmt.Errorf("funding amount is too large, the max "+
|
return nil, fmt.Errorf("funding amount is too large, the max "+
|
||||||
"channel size is: %v", MaxFundingAmount)
|
"channel size is: %v", MaxFundingAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restrict the size of the channel we'll actually open. At a later
|
// 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
|
// level, we'll ensure that the output we create, after accounting for
|
||||||
// fees that a dust output isn't created.
|
// fees, does not leave a dust output. In case of the FundMax flow
|
||||||
if localFundingAmt < funding.MinChanFundingSize {
|
// 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 "+
|
return nil, fmt.Errorf("channel is too small, the minimum "+
|
||||||
"channel size is: %v SAT", int64(funding.MinChanFundingSize))
|
"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
|
// open a new channel. A stream is returned in place, this stream will
|
||||||
// be used to consume updates of the state of the pending channel.
|
// be used to consume updates of the state of the pending channel.
|
||||||
return &funding.InitFundingMsg{
|
return &funding.InitFundingMsg{
|
||||||
TargetPubkey: nodePubKey,
|
TargetPubkey: nodePubKey,
|
||||||
ChainHash: *r.cfg.ActiveNetParams.GenesisHash,
|
ChainHash: *r.cfg.ActiveNetParams.GenesisHash,
|
||||||
LocalFundingAmt: localFundingAmt,
|
LocalFundingAmt: localFundingAmt,
|
||||||
BaseFee: channelBaseFee,
|
BaseFee: channelBaseFee,
|
||||||
FeeRate: channelFeeRate,
|
FeeRate: channelFeeRate,
|
||||||
PushAmt: lnwire.NewMSatFromSatoshis(remoteInitialBalance),
|
PushAmt: lnwire.NewMSatFromSatoshis(
|
||||||
|
remoteInitialBalance,
|
||||||
|
),
|
||||||
MinHtlcIn: minHtlcIn,
|
MinHtlcIn: minHtlcIn,
|
||||||
FundingFeePerKw: feeRate,
|
FundingFeePerKw: feeRate,
|
||||||
Private: in.Private,
|
Private: in.Private,
|
||||||
@@ -2145,6 +2196,8 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
|
|||||||
MaxHtlcs: maxHtlcs,
|
MaxHtlcs: maxHtlcs,
|
||||||
MaxLocalCsv: uint16(in.MaxLocalCsv),
|
MaxLocalCsv: uint16(in.MaxLocalCsv),
|
||||||
ChannelType: channelType,
|
ChannelType: channelType,
|
||||||
|
FundUpToMaxAmt: fundUpToMaxAmt,
|
||||||
|
MinFundAmt: minFundAmt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user