lnd/itest/lnd_channel_funding_fund_max_test.go
2024-10-11 14:02:30 +02:00

334 lines
9.7 KiB
Go

package itest
import (
"context"
"errors"
"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"
)
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 := errors.New(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 := 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 := ht.NewMinerAddress()
// 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)
}