123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 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.00000330 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.00020000 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, 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, 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,
- TargetConf: 6,
- })
- // 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)
- }
|