lnd.xprv/lntest/itest/lnd_funding_test.go

503 lines
17 KiB
Go
Raw Normal View History

package itest
import (
"context"
"fmt"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// testBasicChannelFunding performs a test exercising expected behavior from a
// basic funding workflow. The test creates a new channel between Alice and
// Bob, then immediately closes the channel after asserting some expected post
// conditions. Finally, the chain itself is checked to ensure the closing
// transaction was mined.
func testBasicChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
// Run through the test with combinations of all the different
// commitment types.
allTypes := []commitType{
commitTypeLegacy,
commitTypeTweakless,
commitTypeAnchors,
}
// testFunding is a function closure that takes Carol and Dave's
// commitment types and test the funding flow.
testFunding := func(carolCommitType, daveCommitType commitType) {
// Based on the current tweak variable for Carol, we'll
// preferentially signal the legacy commitment format. We do
// the same for Dave shortly below.
carolArgs := carolCommitType.Args()
carol, err := net.NewNode("Carol", carolArgs)
require.NoError(t.t, err, "unable to create new node")
defer shutdownAndAssert(net, t, carol)
// Each time, we'll send Carol a new set of coins in order to
// fund the channel.
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
require.NoError(t.t, err, "unable to send coins to carol")
daveArgs := daveCommitType.Args()
dave, err := net.NewNode("Dave", daveArgs)
require.NoError(t.t, err, "unable to create new node")
defer shutdownAndAssert(net, t, dave)
// Before we start the test, we'll ensure both sides are
// connected to the funding flow can properly be executed.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = net.EnsureConnected(ctxt, carol, dave)
require.NoError(t.t, err, "unable to connect peers")
carolChan, daveChan, closeChan, err := basicChannelFundingTest(
t, net, carol, dave, nil,
)
require.NoError(t.t, err, "failed funding flow")
// Both nodes should report the same commitment
// type.
chansCommitType := carolChan.CommitmentType
require.Equal(
t.t, chansCommitType,
daveChan.CommitmentType,
"commit types don't match",
)
// Now check that the commitment type reported
// by both nodes is what we expect. It will be
// the minimum of the two nodes' preference, in
// the order Legacy, Tweakless, Anchors.
expType := carolCommitType
switch daveCommitType {
// Dave supports anchors, type will be what
// Carol supports.
case commitTypeAnchors:
// Dave only supports tweakless, channel will
// be downgraded to this type if Carol supports
// anchors.
case commitTypeTweakless:
if expType == commitTypeAnchors {
expType = commitTypeTweakless
}
// Dave only supoprts legacy type, channel will
// be downgraded to this type.
case commitTypeLegacy:
expType = commitTypeLegacy
default:
t.Fatalf("invalid commit type %v", daveCommitType)
}
// Check that the signalled type matches what we
// expect.
switch {
case expType == commitTypeAnchors &&
chansCommitType == lnrpc.CommitmentType_ANCHORS:
case expType == commitTypeTweakless &&
chansCommitType == lnrpc.CommitmentType_STATIC_REMOTE_KEY:
case expType == commitTypeLegacy &&
chansCommitType == lnrpc.CommitmentType_LEGACY:
default:
t.Fatalf("expected nodes to signal "+
"commit type %v, instead got "+
"%v", expType, chansCommitType)
}
// As we've concluded this sub-test case we'll
// now close out the channel for both sides.
closeChan()
}
test:
// We'll test all possible combinations of the feature bit presence
// that both nodes can signal for this new channel type. We'll make a
// new Carol+Dave for each test instance as well.
for _, carolCommitType := range allTypes {
for _, daveCommitType := range allTypes {
cc := carolCommitType
dc := daveCommitType
testName := fmt.Sprintf(
"carol_commit=%v,dave_commit=%v", cc, dc,
)
success := t.t.Run(testName, func(t *testing.T) {
testFunding(cc, dc)
})
if !success {
break test
}
}
}
}
// basicChannelFundingTest is a sub-test of the main testBasicChannelFunding
// test. Given two nodes: Alice and Bob, it'll assert proper channel creation,
// then return a function closure that should be called to assert proper
// channel closure.
func basicChannelFundingTest(t *harnessTest, net *lntest.NetworkHarness,
alice *lntest.HarnessNode, bob *lntest.HarnessNode,
fundingShim *lnrpc.FundingShim) (*lnrpc.Channel, *lnrpc.Channel,
func(), error) {
chanAmt := funding.MaxBtcFundingAmount
pushAmt := btcutil.Amount(100000)
satPerVbyte := btcutil.Amount(1)
// Record nodes' channel balance before testing.
aliceChannelBalance := getChannelBalance(t, alice)
bobChannelBalance := getChannelBalance(t, bob)
// Creates a helper closure to be used below which asserts the proper
// response to a channel balance RPC.
checkChannelBalance := func(node *lntest.HarnessNode,
oldChannelBalance *lnrpc.ChannelBalanceResponse,
local, remote btcutil.Amount) {
newResp := oldChannelBalance
newResp.LocalBalance.Sat += uint64(local)
newResp.LocalBalance.Msat += uint64(
lnwire.NewMSatFromSatoshis(local),
)
newResp.RemoteBalance.Sat += uint64(remote)
newResp.RemoteBalance.Msat += uint64(
lnwire.NewMSatFromSatoshis(remote),
)
// Deprecated fields.
newResp.Balance += int64(local)
assertChannelBalanceResp(t, node, newResp)
}
// First establish a channel with a capacity of 0.5 BTC between Alice
// and Bob with Alice pushing 100k satoshis to Bob's side during
// funding. This function will block until the channel itself is fully
// open or an error occurs in the funding process. A series of
// assertions will be executed to ensure the funding process completed
// successfully.
ctxb := context.Background()
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
chanPoint := openChannelAndAssert(
ctxt, t, net, alice, bob,
lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
FundingShim: fundingShim,
SatPerVByte: satPerVbyte,
},
)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err := alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
require.NoError(t.t, err, "alice didn't report channel")
err = bob.WaitForNetworkChannelOpen(ctxt, chanPoint)
require.NoError(t.t, err, "bob didn't report channel")
cType, err := channelCommitType(alice, chanPoint)
require.NoError(t.t, err, "unable to get channnel type")
// With the channel open, ensure that the amount specified above has
// properly been pushed to Bob.
aliceLocalBalance := chanAmt - pushAmt - cType.calcStaticFee(0)
checkChannelBalance(
alice, aliceChannelBalance, aliceLocalBalance, pushAmt,
)
checkChannelBalance(
bob, bobChannelBalance, pushAmt, aliceLocalBalance,
)
req := &lnrpc.ListChannelsRequest{}
aliceChannel, err := alice.ListChannels(context.Background(), req)
require.NoError(t.t, err, "unable to obtain chan")
bobChannel, err := bob.ListChannels(context.Background(), req)
require.NoError(t.t, err, "unable to obtain chan")
closeChan := func() {
// Finally, immediately close the channel. This function will
// also block until the channel is closed and will additionally
// assert the relevant channel closing post conditions.
ctxt, _ := context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssert(ctxt, t, net, alice, chanPoint, false)
}
return aliceChannel.Channels[0], bobChannel.Channels[0], closeChan, nil
}
// testUnconfirmedChannelFunding tests that our unconfirmed change outputs can
// be used to fund channels.
func testUnconfirmedChannelFunding(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
const (
chanAmt = funding.MaxBtcFundingAmount
pushAmt = btcutil.Amount(100000)
)
// We'll start off by creating a node for Carol.
carol, err := net.NewNode("Carol", nil)
require.NoError(t.t, err, "unable to create carol's node")
defer shutdownAndAssert(net, t, carol)
// We'll send her some confirmed funds.
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
err = net.SendCoins(ctxt, 2*chanAmt, carol)
require.NoError(t.t, err, "unable to send coins to carol")
// Now let Carol send some funds to herself, making a unconfirmed
// change output.
addrReq := &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := carol.NewAddress(ctxt, addrReq)
require.NoError(t.t, err, "unable to get new address")
sendReq := &lnrpc.SendCoinsRequest{
Addr: resp.Address,
Amount: int64(chanAmt) / 5,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = carol.SendCoins(ctxt, sendReq)
require.NoError(t.t, err, "unable to send coins")
// Make sure the unconfirmed tx is seen in the mempool.
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err, "failed to find tx in miner mempool")
// Now, we'll connect her to Alice so that they can open a channel
// together. The funding flow should select Carol's unconfirmed output
// as she doesn't have any other funds since it's a new node.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = net.ConnectNodes(ctxt, carol, net.Alice)
require.NoError(t.t, err, "unable to connect carol to alice")
chanOpenUpdate := openChannelStream(
ctxt, t, net, carol, net.Alice,
lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
SpendUnconfirmed: true,
},
)
// Creates a helper closure to be used below which asserts the proper
// response to a channel balance RPC.
checkChannelBalance := func(node *lntest.HarnessNode,
local, remote, pendingLocal, pendingRemote 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,
)),
},
PendingOpenLocalBalance: &lnrpc.Amount{
Sat: uint64(pendingLocal),
Msat: uint64(lnwire.NewMSatFromSatoshis(
pendingLocal,
)),
},
PendingOpenRemoteBalance: &lnrpc.Amount{
Sat: uint64(pendingRemote),
Msat: uint64(lnwire.NewMSatFromSatoshis(
pendingRemote,
)),
},
UnsettledLocalBalance: &lnrpc.Amount{},
UnsettledRemoteBalance: &lnrpc.Amount{},
// Deprecated fields.
Balance: int64(local),
PendingOpenBalance: int64(pendingLocal),
}
assertChannelBalanceResp(t, node, expectedResponse)
}
// As the channel is pending open, it's expected Carol has both zero
// local and remote balances, and pending local/remote should not be
// zero.
//
// Note that atm we haven't obtained the chanPoint yet, so we use the
// type directly.
cType := commitTypeTweakless
carolLocalBalance := chanAmt - pushAmt - cType.calcStaticFee(0)
checkChannelBalance(carol, 0, 0, carolLocalBalance, pushAmt)
// For Alice, her local/remote balances should be zero, and the
// local/remote balances are the mirror of Carol's.
checkChannelBalance(net.Alice, 0, 0, pushAmt, carolLocalBalance)
// Confirm the channel and wait for it to be recognized by both
// parties. Two transactions should be mined, the unconfirmed spend and
// the funding tx.
mineBlocks(t, net, 6, 2)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
chanPoint, err := net.WaitForChannelOpen(ctxt, chanOpenUpdate)
require.NoError(t.t, err, "error while waitinng for channel open")
// With the channel open, we'll check the balances on each side of the
// channel as a sanity check to ensure things worked out as intended.
checkChannelBalance(carol, carolLocalBalance, pushAmt, 0, 0)
checkChannelBalance(net.Alice, pushAmt, carolLocalBalance, 0, 0)
// Now that we're done with the test, the channel can be closed.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssert(ctxt, t, net, carol, chanPoint, false)
}
// testexternalfundingchanpoint tests that we're able to carry out a normal
// channel funding workflow given a channel point that was constructed outside
// the main daemon.
func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background()
// First, we'll create two new nodes that we'll use to open channel
// between for this test.
carol, err := net.NewNode("carol", nil)
require.NoError(t.t, err)
defer shutdownAndAssert(net, t, carol)
dave, err := net.NewNode("dave", nil)
require.NoError(t.t, err)
defer shutdownAndAssert(net, t, dave)
// Carol will be funding the channel, so we'll send some coins over to
// her and ensure they have enough confirmations before we proceed.
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
require.NoError(t.t, err)
// Before we start the test, we'll ensure both sides are connected to
// the funding flow can properly be executed.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = net.EnsureConnected(ctxt, carol, dave)
require.NoError(t.t, err)
// At this point, we're ready to simulate our external channel funding
// flow. To start with, we'll create a pending channel with a shim for
// a transaction that will never be published.
const thawHeight uint32 = 10
const chanSize = funding.MaxBtcFundingAmount
fundingShim1, chanPoint1, _ := deriveFundingShim(
net, t, carol, dave, chanSize, thawHeight, 1, false,
)
_ = openChannelStream(
ctxb, t, net, carol, dave, lntest.OpenChannelParams{
Amt: chanSize,
FundingShim: fundingShim1,
},
)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
assertNumOpenChannelsPending(ctxt, t, carol, dave, 1)
// That channel is now pending forever and normally would saturate the
// max pending channel limit for both nodes. But because the channel is
// externally funded, we should still be able to open another one. Let's
// do exactly that now. For this one we publish the transaction so we
// can mine it later.
fundingShim2, chanPoint2, _ := deriveFundingShim(
net, t, carol, dave, chanSize, thawHeight, 2, true,
)
// At this point, we'll now carry out the normal basic channel funding
// test as everything should now proceed as normal (a regular channel
// funding flow).
carolChan, daveChan, _, err := basicChannelFundingTest(
t, net, carol, dave, fundingShim2,
)
require.NoError(t.t, err)
// Both channels should be marked as frozen with the proper thaw
// height.
require.Equal(
t.t, thawHeight, carolChan.ThawHeight, "thaw height unmatched",
)
require.Equal(
t.t, thawHeight, daveChan.ThawHeight, "thaw height unmatched",
)
// Next, to make sure the channel functions as normal, we'll make some
// payments within the channel.
payAmt := btcutil.Amount(100000)
invoice := &lnrpc.Invoice{
Memo: "new chans",
Value: int64(payAmt),
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := dave.AddInvoice(ctxt, invoice)
require.NoError(t.t, err)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = completePaymentRequests(
ctxt, carol, carol.RouterClient, []string{resp.PaymentRequest},
true,
)
require.NoError(t.t, err)
// Now that the channels are open, and we've confirmed that they're
// operational, we'll now ensure that the channels are frozen as
// intended (if requested).
//
// First, we'll try to close the channel as Carol, the initiator. This
// should fail as a frozen channel only allows the responder to
// initiate a channel close.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
_, _, err = net.CloseChannel(ctxt, carol, chanPoint2, false)
require.Error(t.t, err,
"carol wasn't denied a co-op close attempt "+
"for a frozen channel",
)
// Next we'll try but this time with Dave (the responder) as the
// initiator. This time the channel should be closed as normal.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssert(ctxt, t, net, dave, chanPoint2, false)
// As a last step, we check if we still have the pending channel hanging
// around because we never published the funding TX.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
assertNumOpenChannelsPending(ctxt, t, carol, dave, 1)
// Let's make sure we can abandon it.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = carol.AbandonChannel(ctxt, &lnrpc.AbandonChannelRequest{
ChannelPoint: chanPoint1,
PendingFundingShimOnly: true,
})
require.NoError(t.t, err)
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = dave.AbandonChannel(ctxt, &lnrpc.AbandonChannelRequest{
ChannelPoint: chanPoint1,
PendingFundingShimOnly: true,
})
require.NoError(t.t, err)
// It should now not appear in the pending channels anymore.
assertNumOpenChannelsPending(ctxt, t, carol, dave, 0)
}