itest: move tests into one file
This commit creates the file lnd_misc_test.go to hold all miscellaneous tests in the file lnd_test.go. From now on, the lnd_test.go will only be responsible for handling the "top" level functionalities such as splitting test cases and run them. Newly created test cases should find their places in the related test files, or create new one when needed.
This commit is contained in:
parent
73a2211205
commit
c912d1aae0
1965
lntest/itest/lnd_misc_test.go
Normal file
1965
lntest/itest/lnd_misc_test.go
Normal file
@ -0,0 +1,1965 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
|
"github.com/lightningnetwork/lnd/funding"
|
||||||
|
"github.com/lightningnetwork/lnd/lncfg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testDisconnectingTargetPeer performs a test which disconnects Alice-peer from
|
||||||
|
// Bob-peer and then re-connects them again. We expect Alice to be able to
|
||||||
|
// disconnect at any point.
|
||||||
|
func testDisconnectingTargetPeer(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// We'll start both nodes with a high backoff so that they don't
|
||||||
|
// reconnect automatically during our test.
|
||||||
|
args := []string{
|
||||||
|
"--minbackoff=1m",
|
||||||
|
"--maxbackoff=1m",
|
||||||
|
}
|
||||||
|
|
||||||
|
alice := net.NewNode(t.t, "Alice", args)
|
||||||
|
defer shutdownAndAssert(net, t, alice)
|
||||||
|
|
||||||
|
bob := net.NewNode(t.t, "Bob", args)
|
||||||
|
defer shutdownAndAssert(net, t, bob)
|
||||||
|
|
||||||
|
// Start by connecting Alice and Bob with no channels.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, alice, bob)
|
||||||
|
|
||||||
|
// Check existing connection.
|
||||||
|
assertNumConnections(t, alice, bob, 1)
|
||||||
|
|
||||||
|
// Give Alice some coins so she can fund a channel.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, alice)
|
||||||
|
|
||||||
|
chanAmt := funding.MaxBtcFundingAmount
|
||||||
|
pushAmt := btcutil.Amount(0)
|
||||||
|
|
||||||
|
// Create a new channel that requires 1 confs before it's considered
|
||||||
|
// open, then broadcast the funding transaction
|
||||||
|
const numConfs = 1
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
pendingUpdate, err := net.OpenPendingChannel(
|
||||||
|
ctxt, alice, bob, chanAmt, pushAmt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to open channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the channel's funding transaction will have been
|
||||||
|
// broadcast, but not confirmed. Alice and Bob's nodes should reflect
|
||||||
|
// this when queried via RPC.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
assertNumOpenChannelsPending(ctxt, t, alice, bob, 1)
|
||||||
|
|
||||||
|
// Disconnect Alice-peer from Bob-peer and get error causes by one
|
||||||
|
// pending channel with detach node is existing.
|
||||||
|
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
||||||
|
t.Fatalf("Bob's peer was disconnected from Alice's"+
|
||||||
|
" while one pending channel is existing: err %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
|
||||||
|
// Assert that the connection was torn down.
|
||||||
|
assertNumConnections(t, alice, bob, 0)
|
||||||
|
|
||||||
|
fundingTxID, err := chainhash.NewHash(pendingUpdate.Txid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to convert funding txid into chainhash.Hash:"+
|
||||||
|
" %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a block, then wait for Alice's node to notify us that the
|
||||||
|
// channel has been opened. The funding transaction should be found
|
||||||
|
// within the newly mined block.
|
||||||
|
block := mineBlocks(t, net, numConfs, 1)[0]
|
||||||
|
assertTxInBlock(t, block, fundingTxID)
|
||||||
|
|
||||||
|
// At this point, the channel should be fully opened and there should be
|
||||||
|
// no pending channels remaining for either node.
|
||||||
|
time.Sleep(time.Millisecond * 300)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
|
||||||
|
assertNumOpenChannelsPending(ctxt, t, alice, bob, 0)
|
||||||
|
|
||||||
|
// Reconnect the nodes so that the channel can become active.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, alice, bob)
|
||||||
|
|
||||||
|
// The channel should be listed in the peer information returned by both
|
||||||
|
// peers.
|
||||||
|
outPoint := wire.OutPoint{
|
||||||
|
Hash: *fundingTxID,
|
||||||
|
Index: pendingUpdate.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check both nodes to ensure that the channel is ready for operation.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.AssertChannelExists(ctxt, alice, &outPoint); err != nil {
|
||||||
|
t.Fatalf("unable to assert channel existence: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.AssertChannelExists(ctxt, bob, &outPoint); err != nil {
|
||||||
|
t.Fatalf("unable to assert channel existence: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect Alice-peer from Bob-peer and get error causes by one
|
||||||
|
// active channel with detach node is existing.
|
||||||
|
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
||||||
|
t.Fatalf("Bob's peer was disconnected from Alice's"+
|
||||||
|
" while one active channel is existing: err %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing connection.
|
||||||
|
assertNumConnections(t, alice, bob, 0)
|
||||||
|
|
||||||
|
// Reconnect both nodes before force closing the channel.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, alice, bob)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
chanPoint := &lnrpc.ChannelPoint{
|
||||||
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
||||||
|
FundingTxidBytes: pendingUpdate.Txid,
|
||||||
|
},
|
||||||
|
OutputIndex: pendingUpdate.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, alice, chanPoint, true)
|
||||||
|
|
||||||
|
// Disconnect Alice-peer from Bob-peer without getting error about
|
||||||
|
// existing channels.
|
||||||
|
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
||||||
|
t.Fatalf("unable to disconnect Bob's peer from Alice's: err %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check zero peer connections.
|
||||||
|
assertNumConnections(t, alice, bob, 0)
|
||||||
|
|
||||||
|
// Finally, re-connect both nodes.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, alice, bob)
|
||||||
|
|
||||||
|
// Check existing connection.
|
||||||
|
assertNumConnections(t, alice, net.Bob, 1)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, alice, chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSphinxReplayPersistence verifies that replayed onion packets are rejected
|
||||||
|
// by a remote peer after a restart. We use a combination of unsafe
|
||||||
|
// configuration arguments to force Carol to replay the same sphinx packet after
|
||||||
|
// reconnecting to Dave, and compare the returned failure message with what we
|
||||||
|
// expect for replayed onion packets.
|
||||||
|
func testSphinxReplayPersistence(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Carol and Dave with Carol being
|
||||||
|
// the sole funder of the channel.
|
||||||
|
chanAmt := btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// First, we'll create Dave, the receiver, and start him in hodl mode.
|
||||||
|
dave := net.NewNode(t.t, "Dave", []string{"--hodl.exit-settle"})
|
||||||
|
|
||||||
|
// We must remember to shutdown the nodes we created for the duration
|
||||||
|
// of the tests, only leaving the two seed nodes (Alice and Bob) within
|
||||||
|
// our test network.
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
// Next, we'll create Carol and establish a channel to from her to
|
||||||
|
// Dave. Carol is started in both unsafe-replay which will cause her to
|
||||||
|
// replay any pending Adds held in memory upon reconnection.
|
||||||
|
carol := net.NewNode(t.t, "Carol", []string{"--unsafe-replay"})
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, carol, dave)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Next, we'll create Fred who is going to initiate the payment and
|
||||||
|
// establish a channel to from him to Carol. We can't perform this test
|
||||||
|
// by paying from Carol directly to Dave, because the '--unsafe-replay'
|
||||||
|
// setup doesn't apply to locally added htlcs. In that case, the
|
||||||
|
// mailbox, that is responsible for generating the replay, is bypassed.
|
||||||
|
fred := net.NewNode(t.t, "Fred", nil)
|
||||||
|
defer shutdownAndAssert(net, t, fred)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, fred, carol)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, fred)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointFC := openChannelAndAssert(
|
||||||
|
ctxt, t, net, fred, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now that the channel is open, create an invoice for Dave which
|
||||||
|
// expects a payment of 1000 satoshis from Carol paid via a particular
|
||||||
|
// preimage.
|
||||||
|
const paymentAmt = 1000
|
||||||
|
preimage := bytes.Repeat([]byte("A"), 32)
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: paymentAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
invoiceResp, err := dave.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all channels to be recognized and advertized.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't advertise channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
err = dave.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't advertise channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointFC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't advertise channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
err = fred.WaitForNetworkChannelOpen(ctxt, chanPointFC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't advertise channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the invoice for Dave added, send a payment from Fred paying
|
||||||
|
// to the above generated invoice.
|
||||||
|
ctx, cancel := context.WithCancel(ctxb)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
payStream, err := fred.RouterClient.SendPaymentV2(
|
||||||
|
ctx,
|
||||||
|
&routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: invoiceResp.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to open payment stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
// Dave's invoice should not be marked as settled.
|
||||||
|
payHash := &lnrpc.PaymentHash{
|
||||||
|
RHash: invoiceResp.RHash,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
dbInvoice, err := dave.LookupInvoice(ctxt, payHash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to lookup invoice: %v", err)
|
||||||
|
}
|
||||||
|
if dbInvoice.Settled {
|
||||||
|
t.Fatalf("dave's invoice should not be marked as settled: %v",
|
||||||
|
spew.Sdump(dbInvoice))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the payment sent but hedl, all balance related stats should not
|
||||||
|
// have changed.
|
||||||
|
err = wait.InvariantNoError(
|
||||||
|
assertAmountSent(0, carol, dave), 3*time.Second,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the first payment sent, restart dave to make sure he is
|
||||||
|
// persisting the information required to detect replayed sphinx
|
||||||
|
// packets.
|
||||||
|
if err := net.RestartNode(dave, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart dave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol should retransmit the Add hedl in her mailbox on startup. Dave
|
||||||
|
// should not accept the replayed Add, and actually fail back the
|
||||||
|
// pending payment. Even though he still holds the original settle, if
|
||||||
|
// he does fail, it is almost certainly caused by the sphinx replay
|
||||||
|
// protection, as it is the only validation we do in hodl mode.
|
||||||
|
result, err := getPaymentResult(payStream)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to receive payment response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that Fred receives the expected failure after Carol sent a
|
||||||
|
// duplicate packet that fails due to sphinx replay detection.
|
||||||
|
if result.Status == lnrpc.Payment_SUCCEEDED {
|
||||||
|
t.Fatalf("expected payment error")
|
||||||
|
}
|
||||||
|
assertLastHTLCError(t, fred, lnrpc.Failure_INVALID_ONION_KEY)
|
||||||
|
|
||||||
|
// Since the payment failed, the balance should still be left
|
||||||
|
// unaltered.
|
||||||
|
err = wait.InvariantNoError(
|
||||||
|
assertAmountSent(0, carol, dave), 3*time.Second,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPoint, true)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, carol, chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testListChannels checks that the response from ListChannels is correct. It
|
||||||
|
// tests the values in all ChannelConstraints are returned as expected. Once
|
||||||
|
// ListChannels becomes mature, a test against all fields in ListChannels should
|
||||||
|
// be performed.
|
||||||
|
func testListChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const aliceRemoteMaxHtlcs = 50
|
||||||
|
const bobRemoteMaxHtlcs = 100
|
||||||
|
|
||||||
|
// Create two fresh nodes and open a channel between them.
|
||||||
|
alice := net.NewNode(t.t, "Alice", nil)
|
||||||
|
defer shutdownAndAssert(net, t, alice)
|
||||||
|
|
||||||
|
bob := net.NewNode(
|
||||||
|
t.t, "Bob", []string{
|
||||||
|
fmt.Sprintf(
|
||||||
|
"--default-remote-max-htlcs=%v",
|
||||||
|
bobRemoteMaxHtlcs,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer shutdownAndAssert(net, t, bob)
|
||||||
|
|
||||||
|
// Connect Alice to Bob.
|
||||||
|
net.ConnectNodes(ctxb, t.t, alice, bob)
|
||||||
|
|
||||||
|
// Give Alice some coins so she can fund a channel.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, alice)
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||||
|
// being the sole funder of the channel. The minial HTLC amount is set to
|
||||||
|
// 4200 msats.
|
||||||
|
const customizedMinHtlc = 4200
|
||||||
|
|
||||||
|
chanAmt := btcutil.Amount(100000)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, alice, bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
MinHtlc: customizedMinHtlc,
|
||||||
|
RemoteMaxHtlcs: aliceRemoteMaxHtlcs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait for Alice and Bob to receive the channel edge from the
|
||||||
|
// funding manager.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err := alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the alice->bob channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = bob.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't see the bob->alice channel before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice should have one channel opened with Bob.
|
||||||
|
assertNodeNumChannels(t, alice, 1)
|
||||||
|
// Bob should have one channel opened with Alice.
|
||||||
|
assertNodeNumChannels(t, bob, 1)
|
||||||
|
|
||||||
|
// Get the ListChannel response from Alice.
|
||||||
|
listReq := &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxb = context.Background()
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := alice.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for %s's channel list: %v",
|
||||||
|
alice.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the returned response is correct.
|
||||||
|
aliceChannel := resp.Channels[0]
|
||||||
|
|
||||||
|
// defaultConstraints is a ChannelConstraints with default values. It is
|
||||||
|
// used to test against Alice's local channel constraints.
|
||||||
|
defaultConstraints := &lnrpc.ChannelConstraints{
|
||||||
|
CsvDelay: 4,
|
||||||
|
ChanReserveSat: 1000,
|
||||||
|
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
|
||||||
|
MaxPendingAmtMsat: 99000000,
|
||||||
|
MinHtlcMsat: 1,
|
||||||
|
MaxAcceptedHtlcs: bobRemoteMaxHtlcs,
|
||||||
|
}
|
||||||
|
assertChannelConstraintsEqual(
|
||||||
|
t, defaultConstraints, aliceChannel.LocalConstraints,
|
||||||
|
)
|
||||||
|
|
||||||
|
// customizedConstraints is a ChannelConstraints with customized values.
|
||||||
|
// Ideally, all these values can be passed in when creating the channel.
|
||||||
|
// Currently, only the MinHtlcMsat is customized. It is used to check
|
||||||
|
// against Alice's remote channel constratins.
|
||||||
|
customizedConstraints := &lnrpc.ChannelConstraints{
|
||||||
|
CsvDelay: 4,
|
||||||
|
ChanReserveSat: 1000,
|
||||||
|
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
|
||||||
|
MaxPendingAmtMsat: 99000000,
|
||||||
|
MinHtlcMsat: customizedMinHtlc,
|
||||||
|
MaxAcceptedHtlcs: aliceRemoteMaxHtlcs,
|
||||||
|
}
|
||||||
|
assertChannelConstraintsEqual(
|
||||||
|
t, customizedConstraints, aliceChannel.RemoteConstraints,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the ListChannel response for Bob.
|
||||||
|
listReq = &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxb = context.Background()
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err = bob.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for %s's channel "+
|
||||||
|
"list: %v", bob.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobChannel := resp.Channels[0]
|
||||||
|
if bobChannel.ChannelPoint != aliceChannel.ChannelPoint {
|
||||||
|
t.Fatalf("Bob's channel point mismatched, want: %s, got: %s",
|
||||||
|
chanPoint.String(), bobChannel.ChannelPoint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check channel constraints match. Alice's local channel constraint should
|
||||||
|
// be equal to Bob's remote channel constraint, and her remote one should
|
||||||
|
// be equal to Bob's local one.
|
||||||
|
assertChannelConstraintsEqual(
|
||||||
|
t, aliceChannel.LocalConstraints, bobChannel.RemoteConstraints,
|
||||||
|
)
|
||||||
|
assertChannelConstraintsEqual(
|
||||||
|
t, aliceChannel.RemoteConstraints, bobChannel.LocalConstraints,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMaxPendingChannels checks that error is returned from remote peer if
|
||||||
|
// max pending channel number was exceeded and that '--maxpendingchannels' flag
|
||||||
|
// exists and works properly.
|
||||||
|
func testMaxPendingChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
maxPendingChannels := lncfg.DefaultMaxPendingChannels + 1
|
||||||
|
amount := funding.MaxBtcFundingAmount
|
||||||
|
|
||||||
|
// Create a new node (Carol) with greater number of max pending
|
||||||
|
// channels.
|
||||||
|
args := []string{
|
||||||
|
fmt.Sprintf("--maxpendingchannels=%v", maxPendingChannels),
|
||||||
|
}
|
||||||
|
carol := net.NewNode(t.t, "Carol", args)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalance := btcutil.Amount(maxPendingChannels) * amount
|
||||||
|
net.SendCoins(ctxt, t.t, carolBalance, carol)
|
||||||
|
|
||||||
|
// Send open channel requests without generating new blocks thereby
|
||||||
|
// increasing pool of pending channels. Then check that we can't open
|
||||||
|
// the channel if the number of pending channels exceed max value.
|
||||||
|
openStreams := make([]lnrpc.Lightning_OpenChannelClient, maxPendingChannels)
|
||||||
|
for i := 0; i < maxPendingChannels; i++ {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
stream := openChannelStream(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
openStreams[i] = stream
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol exhausted available amount of pending channels, next open
|
||||||
|
// channel request should cause ErrorGeneric to be sent back to Alice.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
_, err := net.OpenChannel(
|
||||||
|
ctxt, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("error wasn't received")
|
||||||
|
} else if !strings.Contains(
|
||||||
|
err.Error(), lnwire.ErrMaxPendingChannels.Error(),
|
||||||
|
) {
|
||||||
|
t.Fatalf("not expected error was received: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now our channels are in pending state, in order to not interfere
|
||||||
|
// with other tests we should clean up - complete opening of the
|
||||||
|
// channel and then close it.
|
||||||
|
|
||||||
|
// Mine 6 blocks, then wait for node's to notify us that the channel has
|
||||||
|
// been opened. The funding transactions should be found within the
|
||||||
|
// first newly mined block. 6 blocks make sure the funding transaction
|
||||||
|
// has enough confirmations to be announced publicly.
|
||||||
|
block := mineBlocks(t, net, 6, maxPendingChannels)[0]
|
||||||
|
|
||||||
|
chanPoints := make([]*lnrpc.ChannelPoint, maxPendingChannels)
|
||||||
|
for i, stream := range openStreams {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
fundingChanPoint, err := net.WaitForChannelOpen(ctxt, stream)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error while waiting for channel open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingTxID, err := lnrpc.GetChanPointFundingTxid(fundingChanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the funding transaction enters a block, and is
|
||||||
|
// properly advertised by Alice.
|
||||||
|
assertTxInBlock(t, block, fundingTxID)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Alice.WaitForNetworkChannelOpen(ctxt, fundingChanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("channel not seen on network before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel should be listed in the peer information
|
||||||
|
// returned by both peers.
|
||||||
|
chanPoint := wire.OutPoint{
|
||||||
|
Hash: *fundingTxID,
|
||||||
|
Index: fundingChanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.AssertChannelExists(ctxt, net.Alice, &chanPoint); err != nil {
|
||||||
|
t.Fatalf("unable to assert channel existence: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chanPoints[i] = fundingChanPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, close the channel between Alice and Carol, asserting that the
|
||||||
|
// channel has been properly closed on-chain.
|
||||||
|
for _, chanPoint := range chanPoints {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testGarbageCollectLinkNodes tests that we properly garbase collect link nodes
|
||||||
|
// from the database and the set of persistent connections within the server.
|
||||||
|
func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
chanAmt = 1000000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Bob which will later be
|
||||||
|
// cooperatively closed.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
coopChanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create Carol's node and connect Alice to her.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Carol which will later be force
|
||||||
|
// closed.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
forceCloseChanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now, create Dave's a node and also open a channel between Alice and
|
||||||
|
// him. This link will serve as the only persistent link throughout
|
||||||
|
// restarts in this test.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Alice, dave)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
persistentChanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// isConnected is a helper closure that checks if a peer is connected to
|
||||||
|
// Alice.
|
||||||
|
isConnected := func(pubKey string) bool {
|
||||||
|
req := &lnrpc.ListPeersRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := net.Alice.ListPeers(ctxt, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve alice's peers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range resp.Peers {
|
||||||
|
if peer.PubKey == pubKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart both Bob and Carol to ensure Alice is able to reconnect to
|
||||||
|
// them.
|
||||||
|
if err := net.RestartNode(net.Bob, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart bob's node: %v", err)
|
||||||
|
}
|
||||||
|
if err := net.RestartNode(carol, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart carol's node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Eventually(t.t, func() bool {
|
||||||
|
return isConnected(net.Bob.PubKeyStr)
|
||||||
|
}, defaultTimeout, 20*time.Millisecond)
|
||||||
|
require.Eventually(t.t, func() bool {
|
||||||
|
return isConnected(carol.PubKeyStr)
|
||||||
|
}, defaultTimeout, 20*time.Millisecond)
|
||||||
|
|
||||||
|
// We'll also restart Alice to ensure she can reconnect to her peers
|
||||||
|
// with open channels.
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart alice's node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Eventually(t.t, func() bool {
|
||||||
|
return isConnected(net.Bob.PubKeyStr)
|
||||||
|
}, defaultTimeout, 20*time.Millisecond)
|
||||||
|
require.Eventually(t.t, func() bool {
|
||||||
|
return isConnected(carol.PubKeyStr)
|
||||||
|
}, defaultTimeout, 20*time.Millisecond)
|
||||||
|
require.Eventually(t.t, func() bool {
|
||||||
|
return isConnected(dave.PubKeyStr)
|
||||||
|
}, defaultTimeout, 20*time.Millisecond)
|
||||||
|
err := wait.Predicate(func() bool {
|
||||||
|
return isConnected(dave.PubKeyStr)
|
||||||
|
}, defaultTimeout)
|
||||||
|
|
||||||
|
// testReconnection is a helper closure that restarts the nodes at both
|
||||||
|
// ends of a channel to ensure they do not reconnect after restarting.
|
||||||
|
// When restarting Alice, we'll first need to ensure she has
|
||||||
|
// reestablished her connection with Dave, as they still have an open
|
||||||
|
// channel together.
|
||||||
|
testReconnection := func(node *lntest.HarnessNode) {
|
||||||
|
// Restart both nodes, to trigger the pruning logic.
|
||||||
|
if err := net.RestartNode(node, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart %v's node: %v",
|
||||||
|
node.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart alice's node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now restart both nodes and make sure they don't reconnect.
|
||||||
|
if err := net.RestartNode(node, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart %v's node: %v", node.Name(),
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
err = wait.Invariant(func() bool {
|
||||||
|
return !isConnected(node.PubKeyStr)
|
||||||
|
}, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice reconnected to %v", node.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := net.RestartNode(net.Alice, nil); err != nil {
|
||||||
|
t.Fatalf("unable to restart alice's node: %v", err)
|
||||||
|
}
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
return isConnected(dave.PubKeyStr)
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't reconnect to Dave")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wait.Invariant(func() bool {
|
||||||
|
return !isConnected(node.PubKeyStr)
|
||||||
|
}, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice reconnected to %v", node.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we'll close the channel between Alice and Bob and ensure there
|
||||||
|
// is no reconnection logic between the both once the channel is fully
|
||||||
|
// closed.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, coopChanPoint, false)
|
||||||
|
|
||||||
|
testReconnection(net.Bob)
|
||||||
|
|
||||||
|
// We'll do the same with Alice and Carol, but this time we'll force
|
||||||
|
// close the channel instead.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, forceCloseChanPoint, true)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, net.Alice, forceCloseChanPoint)
|
||||||
|
|
||||||
|
// We'll need to mine some blocks in order to mark the channel fully
|
||||||
|
// closed.
|
||||||
|
_, err = net.Miner.Client.Generate(chainreg.DefaultBitcoinTimeLockDelta - defaultCSV)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we test reconnection, we'll ensure that the channel has been
|
||||||
|
// fully cleaned up for both Carol and Alice.
|
||||||
|
var predErr error
|
||||||
|
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err := net.Alice.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
||||||
|
if predErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
pendingChanResp, err = carol.PendingChannels(
|
||||||
|
ctxt, pendingChansRequest,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to query for pending "+
|
||||||
|
"channels: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
||||||
|
|
||||||
|
return predErr == nil
|
||||||
|
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("channels not marked as fully resolved: %v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
testReconnection(carol)
|
||||||
|
|
||||||
|
// Finally, we'll ensure that Bob and Carol no longer show in Alice's
|
||||||
|
// channel graph.
|
||||||
|
describeGraphReq := &lnrpc.ChannelGraphRequest{
|
||||||
|
IncludeUnannounced: true,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
channelGraph, err := net.Alice.DescribeGraph(ctxt, describeGraphReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to query for alice's channel graph: %v", err)
|
||||||
|
}
|
||||||
|
for _, node := range channelGraph.Nodes {
|
||||||
|
if node.PubKey == net.Bob.PubKeyStr {
|
||||||
|
t.Fatalf("did not expect to find bob in the channel " +
|
||||||
|
"graph, but did")
|
||||||
|
}
|
||||||
|
if node.PubKey == carol.PubKeyStr {
|
||||||
|
t.Fatalf("did not expect to find carol in the channel " +
|
||||||
|
"graph, but did")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the test is done, we can also close the persistent link.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, persistentChanPoint, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testDataLossProtection tests that if one of the nodes in a channel
|
||||||
|
// relationship lost state, they will detect this during channel sync, and the
|
||||||
|
// up-to-date party will force close the channel, giving the outdated party the
|
||||||
|
// opportunity to sweep its output.
|
||||||
|
func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
const (
|
||||||
|
chanAmt = funding.MaxBtcFundingAmount
|
||||||
|
paymentAmt = 10000
|
||||||
|
numInvoices = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// Carol will be the up-to-date party. We set --nolisten to ensure Dave
|
||||||
|
// won't be able to connect to her and trigger the channel data
|
||||||
|
// protection logic automatically. We also can't have Carol
|
||||||
|
// automatically re-connect too early, otherwise DLP would be initiated
|
||||||
|
// at the wrong moment.
|
||||||
|
carol := net.NewNode(
|
||||||
|
t.t, "Carol", []string{"--nolisten", "--minbackoff=1h"},
|
||||||
|
)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Dave will be the party losing his state.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
// Before we make a channel, we'll load up Carol with some coins sent
|
||||||
|
// directly from the miner.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
// timeTravel is a method that will make Carol open a channel to the
|
||||||
|
// passed node, settle a series of payments, then reset the node back
|
||||||
|
// to the state before the payments happened. When this method returns
|
||||||
|
// the node will be unaware of the new state updates. The returned
|
||||||
|
// function can be used to restart the node in this state.
|
||||||
|
timeTravel := func(node *lntest.HarnessNode) (func() error,
|
||||||
|
*lnrpc.ChannelPoint, int64, error) {
|
||||||
|
|
||||||
|
// We must let the node communicate with Carol before they are
|
||||||
|
// able to open channel, so we connect them.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.EnsureConnected(ctxt, t.t, carol, node)
|
||||||
|
|
||||||
|
// We'll first open up a channel between them with a 0.5 BTC
|
||||||
|
// value.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, node,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// With the channel open, we'll create a few invoices for the
|
||||||
|
// node that Carol will pay to in order to advance the state of
|
||||||
|
// the channel.
|
||||||
|
// TODO(halseth): have dangling HTLCs on the commitment, able to
|
||||||
|
// retrieve funds?
|
||||||
|
payReqs, _, _, err := createPayReqs(
|
||||||
|
node, paymentAmt, numInvoices,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Carol to receive the channel edge from the funding
|
||||||
|
// manager.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't see the carol->%s channel "+
|
||||||
|
"before timeout: %v", node.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send payments from Carol using 3 of the payment hashes
|
||||||
|
// generated above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, carol, carol.RouterClient,
|
||||||
|
payReqs[:numInvoices/2], true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next query for the node's channel state, as we sent 3
|
||||||
|
// payments of 10k satoshis each, it should now see his balance
|
||||||
|
// as being 30k satoshis.
|
||||||
|
var nodeChan *lnrpc.Channel
|
||||||
|
var predErr error
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
bChan, err := getChanInfo(ctxt, node)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get channel info: %v", err)
|
||||||
|
}
|
||||||
|
if bChan.LocalBalance != 30000 {
|
||||||
|
predErr = fmt.Errorf("balance is incorrect, "+
|
||||||
|
"got %v, expected %v",
|
||||||
|
bChan.LocalBalance, 30000)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeChan = bChan
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the current commitment height (update number), we'll
|
||||||
|
// later revert him to this state after additional updates to
|
||||||
|
// revoke this state.
|
||||||
|
stateNumPreCopy := nodeChan.NumUpdates
|
||||||
|
|
||||||
|
// With the temporary file created, copy the current state into
|
||||||
|
// the temporary file we created above. Later after more
|
||||||
|
// updates, we'll restore this state.
|
||||||
|
if err := net.BackupDb(node); err != nil {
|
||||||
|
t.Fatalf("unable to copy database files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, send more payments from , using the remaining
|
||||||
|
// payment hashes.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, carol, carol.RouterClient,
|
||||||
|
payReqs[numInvoices/2:], true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
nodeChan, err = getChanInfo(ctxt, node)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave chan info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we shutdown the node, copying over the its temporary
|
||||||
|
// database state which has the *prior* channel state over his
|
||||||
|
// current most up to date state. With this, we essentially
|
||||||
|
// force the node to travel back in time within the channel's
|
||||||
|
// history.
|
||||||
|
if err = net.RestartNode(node, func() error {
|
||||||
|
return net.RestoreDb(node)
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("unable to restart node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the channel is still there from the PoV of the
|
||||||
|
// node.
|
||||||
|
assertNodeNumChannels(t, node, 1)
|
||||||
|
|
||||||
|
// Now query for the channel state, it should show that it's at
|
||||||
|
// a state number in the past, not the *latest* state.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
nodeChan, err = getChanInfo(ctxt, node)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave chan info: %v", err)
|
||||||
|
}
|
||||||
|
if nodeChan.NumUpdates != stateNumPreCopy {
|
||||||
|
t.Fatalf("db copy failed: %v", nodeChan.NumUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
balReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
balResp, err := node.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get dave's balance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
restart, err := net.SuspendNode(node)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to suspend node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return restart, chanPoint, balResp.ConfirmedBalance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset Dave to a state where he has an outdated channel state.
|
||||||
|
restartDave, _, daveStartingBalance, err := timeTravel(dave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to time travel dave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We make a note of the nodes' current on-chain balances, to make sure
|
||||||
|
// they are able to retrieve the channel funds eventually,
|
||||||
|
balReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalResp, err := carol.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
carolStartingBalance := carolBalResp.ConfirmedBalance
|
||||||
|
|
||||||
|
// Restart Dave to trigger a channel resync.
|
||||||
|
if err := restartDave(); err != nil {
|
||||||
|
t.Fatalf("unable to restart dave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that once Dave comes up, they reconnect, Carol force closes
|
||||||
|
// on chain, and both of them properly carry out the DLP protocol.
|
||||||
|
assertDLPExecuted(
|
||||||
|
net, t, carol, carolStartingBalance, dave, daveStartingBalance,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// As a second part of this test, we will test the scenario where a
|
||||||
|
// channel is closed while Dave is offline, loses his state and comes
|
||||||
|
// back online. In this case the node should attempt to resync the
|
||||||
|
// channel, and the peer should resend a channel sync message for the
|
||||||
|
// closed channel, such that Dave can retrieve his funds.
|
||||||
|
//
|
||||||
|
// We start by letting Dave time travel back to an outdated state.
|
||||||
|
restartDave, chanPoint2, daveStartingBalance, err := timeTravel(dave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to time travel eve: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
carolStartingBalance = carolBalResp.ConfirmedBalance
|
||||||
|
|
||||||
|
// Now let Carol force close the channel while Dave is offline.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPoint2, true)
|
||||||
|
|
||||||
|
// Wait for the channel to be marked pending force close.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = waitForChannelPendingForceClose(ctxt, carol, chanPoint2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("channel not pending force close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine enough blocks for Carol to sweep her funds.
|
||||||
|
mineBlocks(t, net, defaultCSV-1, 0)
|
||||||
|
|
||||||
|
carolSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
block := mineBlocks(t, net, 1, 1)[0]
|
||||||
|
assertTxInBlock(t, block, carolSweep)
|
||||||
|
|
||||||
|
// Now the channel should be fully closed also from Carol's POV.
|
||||||
|
assertNumPendingChannels(t, carol, 0, 0)
|
||||||
|
|
||||||
|
// Make sure Carol got her balance back.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get carol's balance: %v", err)
|
||||||
|
}
|
||||||
|
carolBalance := carolBalResp.ConfirmedBalance
|
||||||
|
if carolBalance <= carolStartingBalance {
|
||||||
|
t.Fatalf("expected carol to have balance above %d, "+
|
||||||
|
"instead had %v", carolStartingBalance,
|
||||||
|
carolBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNodeNumChannels(t, carol, 0)
|
||||||
|
|
||||||
|
// When Dave comes online, he will reconnect to Carol, try to resync
|
||||||
|
// the channel, but it will already be closed. Carol should resend the
|
||||||
|
// information Dave needs to sweep his funds.
|
||||||
|
if err := restartDave(); err != nil {
|
||||||
|
t.Fatalf("unable to restart Eve: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dave should sweep his funds.
|
||||||
|
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a block to confirm the sweep, and make sure Dave got his
|
||||||
|
// balance back.
|
||||||
|
mineBlocks(t, net, 1, 1)
|
||||||
|
assertNodeNumChannels(t, dave, 0)
|
||||||
|
|
||||||
|
err = wait.NoError(func() error {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
daveBalResp, err := dave.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get dave's balance: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daveBalance := daveBalResp.ConfirmedBalance
|
||||||
|
if daveBalance <= daveStartingBalance {
|
||||||
|
return fmt.Errorf("expected dave to have balance "+
|
||||||
|
"above %d, intead had %v", daveStartingBalance,
|
||||||
|
daveBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testRejectHTLC tests that a node can be created with the flag --rejecthtlc.
|
||||||
|
// This means that the node will reject all forwarded HTLCs but can still
|
||||||
|
// accept direct HTLCs as well as send HTLCs.
|
||||||
|
func testRejectHTLC(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
// RejectHTLC
|
||||||
|
// Alice ------> Carol ------> Bob
|
||||||
|
//
|
||||||
|
const chanAmt = btcutil.Amount(1000000)
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// Create Carol with reject htlc flag.
|
||||||
|
carol := net.NewNode(t.t, "Carol", []string{"--rejecthtlc"})
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Connect Alice to Carol.
|
||||||
|
net.ConnectNodes(ctxb, t.t, net.Alice, carol)
|
||||||
|
|
||||||
|
// Connect Carol to Bob.
|
||||||
|
net.ConnectNodes(ctxb, t.t, carol, net.Bob)
|
||||||
|
|
||||||
|
// Send coins to Carol.
|
||||||
|
net.SendCoins(ctxb, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
// Send coins to Alice.
|
||||||
|
net.SendCoins(ctxb, t.t, btcutil.SatoshiPerBitcent, net.Alice)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Carol.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel between Carol and Bob.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Channel should be ready for payments.
|
||||||
|
const payAmt = 100
|
||||||
|
|
||||||
|
// Helper closure to generate a random pre image.
|
||||||
|
genPreImage := func() []byte {
|
||||||
|
preimage := make([]byte, 32)
|
||||||
|
|
||||||
|
_, err := rand.Read(preimage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate preimage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return preimage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an invoice from Carol of 100 satoshis.
|
||||||
|
// We expect Alice to be able to pay this invoice.
|
||||||
|
preimage := genPreImage()
|
||||||
|
|
||||||
|
carolInvoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing - alice should pay carol",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: payAmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol adds the invoice to her database.
|
||||||
|
resp, err := carol.AddInvoice(ctxb, carolInvoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice pays Carols invoice.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient,
|
||||||
|
[]string{resp.PaymentRequest}, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments from alice to carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an invoice from Bob of 100 satoshis.
|
||||||
|
// We expect Carol to be able to pay this invoice.
|
||||||
|
preimage = genPreImage()
|
||||||
|
|
||||||
|
bobInvoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing - carol should pay bob",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: payAmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob adds the invoice to his database.
|
||||||
|
resp, err = net.Bob.AddInvoice(ctxb, bobInvoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carol pays Bobs invoice.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, carol, carol.RouterClient,
|
||||||
|
[]string{resp.PaymentRequest}, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments from carol to bob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an invoice from Bob of 100 satoshis.
|
||||||
|
// Alice attempts to pay Bob but this should fail, since we are
|
||||||
|
// using Carol as a hop and her node will reject onward HTLCs.
|
||||||
|
preimage = genPreImage()
|
||||||
|
|
||||||
|
bobInvoice = &lnrpc.Invoice{
|
||||||
|
Memo: "testing - alice tries to pay bob",
|
||||||
|
RPreimage: preimage,
|
||||||
|
Value: payAmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob adds the invoice to his database.
|
||||||
|
resp, err = net.Bob.AddInvoice(ctxb, bobInvoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice attempts to pay Bobs invoice. This payment should be rejected since
|
||||||
|
// we are using Carol as an intermediary hop, Carol is running lnd with
|
||||||
|
// --rejecthtlc.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient,
|
||||||
|
[]string{resp.PaymentRequest}, true,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"should have been rejected, carol will not accept forwarded htlcs",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertLastHTLCError(t, net.Alice, lnrpc.Failure_CHANNEL_DISABLED)
|
||||||
|
|
||||||
|
// Close all channels.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNodeSignVerify(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
chanAmt := funding.MaxBtcFundingAmount
|
||||||
|
pushAmt := btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Create a channel between alice and bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
aliceBobCh := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: pushAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
aliceMsg := []byte("alice msg")
|
||||||
|
|
||||||
|
// alice signs "alice msg" and sends her signature to bob.
|
||||||
|
sigReq := &lnrpc.SignMessageRequest{Msg: aliceMsg}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
sigResp, err := net.Alice.SignMessage(ctxt, sigReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SignMessage rpc call failed: %v", err)
|
||||||
|
}
|
||||||
|
aliceSig := sigResp.Signature
|
||||||
|
|
||||||
|
// bob verifying alice's signature should succeed since alice and bob are
|
||||||
|
// connected.
|
||||||
|
verifyReq := &lnrpc.VerifyMessageRequest{Msg: aliceMsg, Signature: aliceSig}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
verifyResp, err := net.Bob.VerifyMessage(ctxt, verifyReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VerifyMessage failed: %v", err)
|
||||||
|
}
|
||||||
|
if !verifyResp.Valid {
|
||||||
|
t.Fatalf("alice's signature didn't validate")
|
||||||
|
}
|
||||||
|
if verifyResp.Pubkey != net.Alice.PubKeyStr {
|
||||||
|
t.Fatalf("alice's signature doesn't contain alice's pubkey.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// carol is a new node that is unconnected to alice or bob.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
carolMsg := []byte("carol msg")
|
||||||
|
|
||||||
|
// carol signs "carol msg" and sends her signature to bob.
|
||||||
|
sigReq = &lnrpc.SignMessageRequest{Msg: carolMsg}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
sigResp, err = carol.SignMessage(ctxt, sigReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SignMessage rpc call failed: %v", err)
|
||||||
|
}
|
||||||
|
carolSig := sigResp.Signature
|
||||||
|
|
||||||
|
// bob verifying carol's signature should fail since they are not connected.
|
||||||
|
verifyReq = &lnrpc.VerifyMessageRequest{Msg: carolMsg, Signature: carolSig}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
verifyResp, err = net.Bob.VerifyMessage(ctxt, verifyReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VerifyMessage failed: %v", err)
|
||||||
|
}
|
||||||
|
if verifyResp.Valid {
|
||||||
|
t.Fatalf("carol's signature should not be valid")
|
||||||
|
}
|
||||||
|
if verifyResp.Pubkey != carol.PubKeyStr {
|
||||||
|
t.Fatalf("carol's signature doesn't contain her pubkey")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the channel between alice and bob.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, aliceBobCh, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSendUpdateDisableChannel ensures that a channel update with the disable
|
||||||
|
// flag set is sent once a channel has been either unilaterally or cooperatively
|
||||||
|
// closed.
|
||||||
|
func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const (
|
||||||
|
chanAmt = 100000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Bob and Alice and Carol. These will
|
||||||
|
// be closed later on in order to trigger channel update messages
|
||||||
|
// marking the channels as disabled.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAliceBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
carol := net.NewNode(
|
||||||
|
t.t, "Carol", []string{
|
||||||
|
"--minbackoff=10s",
|
||||||
|
"--chan-enable-timeout=1.5s",
|
||||||
|
"--chan-disable-timeout=3s",
|
||||||
|
"--chan-status-sample-interval=.5s",
|
||||||
|
})
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAliceCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// We create a new node Eve that has an inactive channel timeout of
|
||||||
|
// just 2 seconds (down from the default 20m). It will be used to test
|
||||||
|
// channel updates for channels going inactive.
|
||||||
|
eve := net.NewNode(
|
||||||
|
t.t, "Eve", []string{
|
||||||
|
"--minbackoff=10s",
|
||||||
|
"--chan-enable-timeout=1.5s",
|
||||||
|
"--chan-disable-timeout=3s",
|
||||||
|
"--chan-status-sample-interval=.5s",
|
||||||
|
})
|
||||||
|
defer shutdownAndAssert(net, t, eve)
|
||||||
|
|
||||||
|
// Give Eve some coins.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, eve)
|
||||||
|
|
||||||
|
// Connect Eve to Carol and Bob, and open a channel to carol.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, eve, carol)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, eve, net.Bob)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointEveCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, eve, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Launch a node for Dave which will connect to Bob in order to receive
|
||||||
|
// graph updates from. This will ensure that the channel updates are
|
||||||
|
// propagated throughout the network.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.ConnectNodes(ctxt, t.t, net.Bob, dave)
|
||||||
|
|
||||||
|
daveSub := subscribeGraphNotifications(ctxb, t, dave)
|
||||||
|
defer close(daveSub.quit)
|
||||||
|
|
||||||
|
// We should expect to see a channel update with the default routing
|
||||||
|
// policy, except that it should indicate the channel is disabled.
|
||||||
|
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||||
|
FeeBaseMsat: int64(chainreg.DefaultBitcoinBaseFeeMSat),
|
||||||
|
FeeRateMilliMsat: int64(chainreg.DefaultBitcoinFeeRate),
|
||||||
|
TimeLockDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||||
|
MinHtlc: 1000, // default value
|
||||||
|
MaxHtlcMsat: calculateMaxHtlc(chanAmt),
|
||||||
|
Disabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let Carol go offline. Since Eve has an inactive timeout of 2s, we
|
||||||
|
// expect her to send an update disabling the channel.
|
||||||
|
restartCarol, err := net.SuspendNode(carol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to suspend carol: %v", err)
|
||||||
|
}
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// We restart Carol. Since the channel now becomes active again, Eve
|
||||||
|
// should send a ChannelUpdate setting the channel no longer disabled.
|
||||||
|
if err := restartCarol(); err != nil {
|
||||||
|
t.Fatalf("unable to restart carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPolicy.Disabled = false
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now we'll test a long disconnection. Disconnect Carol and Eve and
|
||||||
|
// ensure they both detect each other as disabled. Their min backoffs
|
||||||
|
// are high enough to not interfere with disabling logic.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.DisconnectNodes(ctxt, carol, eve); err != nil {
|
||||||
|
t.Fatalf("unable to disconnect Carol from Eve: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a disable from both Carol and Eve to come through.
|
||||||
|
expectedPolicy.Disabled = true
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
{carol.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reconnect Carol and Eve, this should cause them to reenable the
|
||||||
|
// channel from both ends after a short delay.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.EnsureConnected(ctxt, t.t, carol, eve)
|
||||||
|
|
||||||
|
expectedPolicy.Disabled = false
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
{carol.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now we'll test a short disconnection. Disconnect Carol and Eve, then
|
||||||
|
// reconnect them after one second so that their scheduled disables are
|
||||||
|
// aborted. One second is twice the status sample interval, so this
|
||||||
|
// should allow for the disconnect to be detected, but still leave time
|
||||||
|
// to cancel the announcement before the 3 second inactive timeout is
|
||||||
|
// hit.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.DisconnectNodes(ctxt, carol, eve); err != nil {
|
||||||
|
t.Fatalf("unable to disconnect Carol from Eve: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.EnsureConnected(ctxt, t.t, eve, carol)
|
||||||
|
|
||||||
|
// Since the disable should have been canceled by both Carol and Eve, we
|
||||||
|
// expect no channel updates to appear on the network.
|
||||||
|
assertNoChannelUpdates(t, daveSub, 4*time.Second)
|
||||||
|
|
||||||
|
// Close Alice's channels with Bob and Carol cooperatively and
|
||||||
|
// unilaterally respectively.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
_, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceBob, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to close channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
_, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceCarol, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to close channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel close processes have been started, we should
|
||||||
|
// receive an update marking each as disabled.
|
||||||
|
expectedPolicy.Disabled = true
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{net.Alice.PubKeyStr, expectedPolicy, chanPointAliceBob},
|
||||||
|
{net.Alice.PubKeyStr, expectedPolicy, chanPointAliceCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, close the channels by mining the closing transactions.
|
||||||
|
mineBlocks(t, net, 1, 2)
|
||||||
|
|
||||||
|
// Also do this check for Eve's channel with Carol.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
_, _, err = net.CloseChannel(ctxt, eve, chanPointEveCarol, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to close channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, daveSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mineBlocks(t, net, 1, 1)
|
||||||
|
|
||||||
|
// And finally, clean up the force closed channel by mining the
|
||||||
|
// sweeping transaction.
|
||||||
|
cleanupForceClose(t, net, net.Alice, chanPointAliceCarol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testAbandonChannel abandones a channel and asserts that it is no
|
||||||
|
// longer open and not in one of the pending closure states. It also
|
||||||
|
// verifies that the abandoned channel is reported as closed with close
|
||||||
|
// type 'abandoned'.
|
||||||
|
func testAbandonChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// First establish a channel between Alice and Bob.
|
||||||
|
channelParam := lntest.OpenChannelParams{
|
||||||
|
Amt: funding.MaxBtcFundingAmount,
|
||||||
|
PushAmt: btcutil.Amount(100000),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPoint := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob, channelParam,
|
||||||
|
)
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
chanPointStr := fmt.Sprintf("%v:%v", txid, chanPoint.OutputIndex)
|
||||||
|
|
||||||
|
// Wait for channel to be confirmed open.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't report channel: %v", err)
|
||||||
|
}
|
||||||
|
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't report channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channel is open, we'll obtain its channel ID real quick
|
||||||
|
// so we can use it to query the graph below.
|
||||||
|
listReq := &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceChannelList, err := net.Alice.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch alice's channels: %v", err)
|
||||||
|
}
|
||||||
|
var chanID uint64
|
||||||
|
for _, channel := range aliceChannelList.Channels {
|
||||||
|
if channel.ChannelPoint == chanPointStr {
|
||||||
|
chanID = channel.ChanId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chanID == 0 {
|
||||||
|
t.Fatalf("unable to find channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// To make sure the channel is removed from the backup file as well when
|
||||||
|
// being abandoned, grab a backup snapshot so we can compare it with the
|
||||||
|
// later state.
|
||||||
|
bkupBefore, err := ioutil.ReadFile(net.Alice.ChanBackupPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get channel backup before abandoning "+
|
||||||
|
"channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request to abandon channel.
|
||||||
|
abandonChannelRequest := &lnrpc.AbandonChannelRequest{
|
||||||
|
ChannelPoint: chanPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = net.Alice.AbandonChannel(ctxt, abandonChannelRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to abandon channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that channel in no longer open.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceChannelList, err = net.Alice.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to list channels: %v", err)
|
||||||
|
}
|
||||||
|
if len(aliceChannelList.Channels) != 0 {
|
||||||
|
t.Fatalf("alice should only have no channels open, "+
|
||||||
|
"instead she has %v",
|
||||||
|
len(aliceChannelList.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that channel is not pending closure.
|
||||||
|
pendingReq := &lnrpc.PendingChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
alicePendingList, err := net.Alice.PendingChannels(ctxt, pendingReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to list pending channels: %v", err)
|
||||||
|
}
|
||||||
|
if len(alicePendingList.PendingClosingChannels) != 0 { //nolint:staticcheck
|
||||||
|
t.Fatalf("alice should only have no pending closing channels, "+
|
||||||
|
"instead she has %v",
|
||||||
|
len(alicePendingList.PendingClosingChannels)) //nolint:staticcheck
|
||||||
|
}
|
||||||
|
if len(alicePendingList.PendingForceClosingChannels) != 0 {
|
||||||
|
t.Fatalf("alice should only have no pending force closing "+
|
||||||
|
"channels instead she has %v",
|
||||||
|
len(alicePendingList.PendingForceClosingChannels))
|
||||||
|
}
|
||||||
|
if len(alicePendingList.WaitingCloseChannels) != 0 {
|
||||||
|
t.Fatalf("alice should only have no waiting close "+
|
||||||
|
"channels instead she has %v",
|
||||||
|
len(alicePendingList.WaitingCloseChannels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that channel is listed as abandoned.
|
||||||
|
closedReq := &lnrpc.ClosedChannelsRequest{
|
||||||
|
Abandoned: true,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceClosedList, err := net.Alice.ClosedChannels(ctxt, closedReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to list closed channels: %v", err)
|
||||||
|
}
|
||||||
|
if len(aliceClosedList.Channels) != 1 {
|
||||||
|
t.Fatalf("alice should only have a single abandoned channel, "+
|
||||||
|
"instead she has %v",
|
||||||
|
len(aliceClosedList.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the channel can no longer be found in the channel graph.
|
||||||
|
_, err = net.Alice.GetChanInfo(ctxb, &lnrpc.ChanInfoRequest{
|
||||||
|
ChanId: chanID,
|
||||||
|
})
|
||||||
|
if !strings.Contains(err.Error(), "marked as zombie") {
|
||||||
|
t.Fatalf("channel shouldn't be found in the channel " +
|
||||||
|
"graph!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the channel is no longer in the channel backup list.
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
bkupAfter, err := ioutil.ReadFile(net.Alice.ChanBackupPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get channel backup before "+
|
||||||
|
"abandoning channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(bkupAfter) < len(bkupBefore)
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("channel wasn't removed from channel backup file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calling AbandonChannel again, should result in no new errors, as the
|
||||||
|
// channel has already been removed.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = net.Alice.AbandonChannel(ctxt, abandonChannelRequest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to abandon channel a second time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we're done with the test, the channel can be closed. This
|
||||||
|
// is necessary to avoid unexpected outcomes of other tests that use
|
||||||
|
// Bob's lnd instance.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPoint, true)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, net.Bob, chanPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSweepAllCoins tests that we're able to properly sweep all coins from the
|
||||||
|
// wallet into a single target address at the specified fee rate.
|
||||||
|
func testSweepAllCoins(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// First, we'll make a new node, ainz who'll we'll use to test wallet
|
||||||
|
// sweeping.
|
||||||
|
ainz := net.NewNode(t.t, "Ainz", nil)
|
||||||
|
defer shutdownAndAssert(net, t, ainz)
|
||||||
|
|
||||||
|
// Next, we'll give Ainz exactly 2 utxos of 1 BTC each, with one of
|
||||||
|
// them being p2wkh and the other being a n2wpkh address.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, ainz)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoinsNP2WKH(ctxt, t.t, btcutil.SatoshiPerBitcoin, ainz)
|
||||||
|
|
||||||
|
// Ensure that we can't send coins to our own Pubkey.
|
||||||
|
info, err := ainz.GetInfo(ctxt, &lnrpc.GetInfoRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get node info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a label that we will used to label the transaction with.
|
||||||
|
sendCoinsLabel := "send all coins"
|
||||||
|
|
||||||
|
sweepReq := &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: info.IdentityPubkey,
|
||||||
|
SendAll: true,
|
||||||
|
Label: sendCoinsLabel,
|
||||||
|
}
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected SendCoins to users own pubkey to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that we can't send coins to another users Pubkey.
|
||||||
|
info, err = net.Alice.GetInfo(ctxt, &lnrpc.GetInfoRequest{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get node info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepReq = &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: info.IdentityPubkey,
|
||||||
|
SendAll: true,
|
||||||
|
Label: sendCoinsLabel,
|
||||||
|
}
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected SendCoins to Alices pubkey to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the two coins above mined, we'll now instruct ainz to sweep all
|
||||||
|
// the coins to an external address not under its control.
|
||||||
|
// We will first attempt to send the coins to addresses that are not
|
||||||
|
// compatible with the current network. This is to test that the wallet
|
||||||
|
// will prevent any onchain transactions to addresses that are not on the
|
||||||
|
// same network as the user.
|
||||||
|
|
||||||
|
// Send coins to a testnet3 address.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
sweepReq = &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: "tb1qfc8fusa98jx8uvnhzavxccqlzvg749tvjw82tg",
|
||||||
|
SendAll: true,
|
||||||
|
Label: sendCoinsLabel,
|
||||||
|
}
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected SendCoins to different network to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send coins to a mainnet address.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
sweepReq = &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: "1MPaXKp5HhsLNjVSqaL7fChE3TVyrTMRT3",
|
||||||
|
SendAll: true,
|
||||||
|
Label: sendCoinsLabel,
|
||||||
|
}
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected SendCoins to different network to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send coins to a compatible address.
|
||||||
|
minerAddr, err := net.Miner.NewAddress()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create new miner addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepReq = &lnrpc.SendCoinsRequest{
|
||||||
|
Addr: minerAddr.String(),
|
||||||
|
SendAll: true,
|
||||||
|
Label: sendCoinsLabel,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to sweep coins: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll mine a block which should include the sweep transaction we
|
||||||
|
// generated above.
|
||||||
|
block := mineBlocks(t, net, 1, 1)[0]
|
||||||
|
|
||||||
|
// The sweep transaction should have exactly two inputs as we only had
|
||||||
|
// two UTXOs in the wallet.
|
||||||
|
sweepTx := block.Transactions[1]
|
||||||
|
if len(sweepTx.TxIn) != 2 {
|
||||||
|
t.Fatalf("expected 2 inputs instead have %v", len(sweepTx.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTxStr := sweepTx.TxHash().String()
|
||||||
|
assertTxLabel(ctxb, t, ainz, sweepTxStr, sendCoinsLabel)
|
||||||
|
|
||||||
|
// While we are looking at labels, we test our label transaction command
|
||||||
|
// to make sure it is behaving as expected. First, we try to label our
|
||||||
|
// transaction with an empty label, and check that we fail as expected.
|
||||||
|
sweepHash := sweepTx.TxHash()
|
||||||
|
_, err = ainz.WalletKitClient.LabelTransaction(
|
||||||
|
ctxt, &walletrpc.LabelTransactionRequest{
|
||||||
|
Txid: sweepHash[:],
|
||||||
|
Label: "",
|
||||||
|
Overwrite: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for zero transaction label")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our error will be wrapped in a rpc error, so we check that it
|
||||||
|
// contains the error we expect.
|
||||||
|
errZeroLabel := "cannot label transaction with empty label"
|
||||||
|
if !strings.Contains(err.Error(), errZeroLabel) {
|
||||||
|
t.Fatalf("expected: zero label error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we try to relabel our transaction without setting the overwrite
|
||||||
|
// boolean. We expect this to fail, because the wallet requires setting
|
||||||
|
// of this param to prevent accidental overwrite of labels.
|
||||||
|
_, err = ainz.WalletKitClient.LabelTransaction(
|
||||||
|
ctxt, &walletrpc.LabelTransactionRequest{
|
||||||
|
Txid: sweepHash[:],
|
||||||
|
Label: "label that will not work",
|
||||||
|
Overwrite: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for tx already labelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our error will be wrapped in a rpc error, so we check that it
|
||||||
|
// contains the error we expect.
|
||||||
|
if !strings.Contains(err.Error(), wallet.ErrTxLabelExists.Error()) {
|
||||||
|
t.Fatalf("expected: label exists, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we overwrite our label with a new label, which should not
|
||||||
|
// fail.
|
||||||
|
newLabel := "new sweep tx label"
|
||||||
|
_, err = ainz.WalletKitClient.LabelTransaction(
|
||||||
|
ctxt, &walletrpc.LabelTransactionRequest{
|
||||||
|
Txid: sweepHash[:],
|
||||||
|
Label: newLabel,
|
||||||
|
Overwrite: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not label tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTxLabel(ctxb, t, ainz, sweepTxStr, newLabel)
|
||||||
|
|
||||||
|
// Finally, Ainz should now have no coins at all within his wallet.
|
||||||
|
balReq := &lnrpc.WalletBalanceRequest{}
|
||||||
|
resp, err := ainz.WalletBalance(ctxt, balReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get ainz's balance: %v", err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case resp.ConfirmedBalance != 0:
|
||||||
|
t.Fatalf("expected no confirmed balance, instead have %v",
|
||||||
|
resp.ConfirmedBalance)
|
||||||
|
|
||||||
|
case resp.UnconfirmedBalance != 0:
|
||||||
|
t.Fatalf("expected no unconfirmed balance, instead have %v",
|
||||||
|
resp.UnconfirmedBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we try again, but this time specifying an amount, then the call
|
||||||
|
// should fail.
|
||||||
|
sweepReq.Amount = 10000
|
||||||
|
_, err = ainz.SendCoins(ctxt, sweepReq)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("sweep attempt should fail")
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +1,17 @@
|
|||||||
package itest
|
package itest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/integration/rpctest"
|
"github.com/btcsuite/btcd/integration/rpctest"
|
||||||
"github.com/btcsuite/btcd/rpcclient"
|
"github.com/btcsuite/btcd/rpcclient"
|
||||||
"github.com/btcsuite/btcd/wire"
|
|
||||||
"github.com/btcsuite/btcutil"
|
|
||||||
"github.com/btcsuite/btcwallet/wallet"
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/lightningnetwork/lnd/chainreg"
|
|
||||||
"github.com/lightningnetwork/lnd/funding"
|
|
||||||
"github.com/lightningnetwork/lnd/lncfg"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -98,1943 +81,6 @@ func getTestCaseSplitTranche() ([]*testCase, uint, uint) {
|
|||||||
return allTestCases[trancheOffset:trancheEnd], threadID, trancheOffset
|
return allTestCases[trancheOffset:trancheEnd], threadID, trancheOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
// testDisconnectingTargetPeer performs a test which disconnects Alice-peer from
|
|
||||||
// Bob-peer and then re-connects them again. We expect Alice to be able to
|
|
||||||
// disconnect at any point.
|
|
||||||
func testDisconnectingTargetPeer(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// We'll start both nodes with a high backoff so that they don't
|
|
||||||
// reconnect automatically during our test.
|
|
||||||
args := []string{
|
|
||||||
"--minbackoff=1m",
|
|
||||||
"--maxbackoff=1m",
|
|
||||||
}
|
|
||||||
|
|
||||||
alice := net.NewNode(t.t, "Alice", args)
|
|
||||||
defer shutdownAndAssert(net, t, alice)
|
|
||||||
|
|
||||||
bob := net.NewNode(t.t, "Bob", args)
|
|
||||||
defer shutdownAndAssert(net, t, bob)
|
|
||||||
|
|
||||||
// Start by connecting Alice and Bob with no channels.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, alice, bob)
|
|
||||||
|
|
||||||
// Check existing connection.
|
|
||||||
assertNumConnections(t, alice, bob, 1)
|
|
||||||
|
|
||||||
// Give Alice some coins so she can fund a channel.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, alice)
|
|
||||||
|
|
||||||
chanAmt := funding.MaxBtcFundingAmount
|
|
||||||
pushAmt := btcutil.Amount(0)
|
|
||||||
|
|
||||||
// Create a new channel that requires 1 confs before it's considered
|
|
||||||
// open, then broadcast the funding transaction
|
|
||||||
const numConfs = 1
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
pendingUpdate, err := net.OpenPendingChannel(
|
|
||||||
ctxt, alice, bob, chanAmt, pushAmt,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to open channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, the channel's funding transaction will have been
|
|
||||||
// broadcast, but not confirmed. Alice and Bob's nodes should reflect
|
|
||||||
// this when queried via RPC.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
assertNumOpenChannelsPending(ctxt, t, alice, bob, 1)
|
|
||||||
|
|
||||||
// Disconnect Alice-peer from Bob-peer and get error causes by one
|
|
||||||
// pending channel with detach node is existing.
|
|
||||||
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
|
||||||
t.Fatalf("Bob's peer was disconnected from Alice's"+
|
|
||||||
" while one pending channel is existing: err %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 300)
|
|
||||||
|
|
||||||
// Assert that the connection was torn down.
|
|
||||||
assertNumConnections(t, alice, bob, 0)
|
|
||||||
|
|
||||||
fundingTxID, err := chainhash.NewHash(pendingUpdate.Txid)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to convert funding txid into chainhash.Hash:"+
|
|
||||||
" %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine a block, then wait for Alice's node to notify us that the
|
|
||||||
// channel has been opened. The funding transaction should be found
|
|
||||||
// within the newly mined block.
|
|
||||||
block := mineBlocks(t, net, numConfs, 1)[0]
|
|
||||||
assertTxInBlock(t, block, fundingTxID)
|
|
||||||
|
|
||||||
// At this point, the channel should be fully opened and there should be
|
|
||||||
// no pending channels remaining for either node.
|
|
||||||
time.Sleep(time.Millisecond * 300)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
|
|
||||||
assertNumOpenChannelsPending(ctxt, t, alice, bob, 0)
|
|
||||||
|
|
||||||
// Reconnect the nodes so that the channel can become active.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, alice, bob)
|
|
||||||
|
|
||||||
// The channel should be listed in the peer information returned by both
|
|
||||||
// peers.
|
|
||||||
outPoint := wire.OutPoint{
|
|
||||||
Hash: *fundingTxID,
|
|
||||||
Index: pendingUpdate.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check both nodes to ensure that the channel is ready for operation.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.AssertChannelExists(ctxt, alice, &outPoint); err != nil {
|
|
||||||
t.Fatalf("unable to assert channel existence: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.AssertChannelExists(ctxt, bob, &outPoint); err != nil {
|
|
||||||
t.Fatalf("unable to assert channel existence: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect Alice-peer from Bob-peer and get error causes by one
|
|
||||||
// active channel with detach node is existing.
|
|
||||||
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
|
||||||
t.Fatalf("Bob's peer was disconnected from Alice's"+
|
|
||||||
" while one active channel is existing: err %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check existing connection.
|
|
||||||
assertNumConnections(t, alice, bob, 0)
|
|
||||||
|
|
||||||
// Reconnect both nodes before force closing the channel.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, alice, bob)
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
chanPoint := &lnrpc.ChannelPoint{
|
|
||||||
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
||||||
FundingTxidBytes: pendingUpdate.Txid,
|
|
||||||
},
|
|
||||||
OutputIndex: pendingUpdate.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, alice, chanPoint, true)
|
|
||||||
|
|
||||||
// Disconnect Alice-peer from Bob-peer without getting error about
|
|
||||||
// existing channels.
|
|
||||||
if err := net.DisconnectNodes(ctxt, alice, bob); err != nil {
|
|
||||||
t.Fatalf("unable to disconnect Bob's peer from Alice's: err %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check zero peer connections.
|
|
||||||
assertNumConnections(t, alice, bob, 0)
|
|
||||||
|
|
||||||
// Finally, re-connect both nodes.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, alice, bob)
|
|
||||||
|
|
||||||
// Check existing connection.
|
|
||||||
assertNumConnections(t, alice, net.Bob, 1)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, alice, chanPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSphinxReplayPersistence verifies that replayed onion packets are rejected
|
|
||||||
// by a remote peer after a restart. We use a combination of unsafe
|
|
||||||
// configuration arguments to force Carol to replay the same sphinx packet after
|
|
||||||
// reconnecting to Dave, and compare the returned failure message with what we
|
|
||||||
// expect for replayed onion packets.
|
|
||||||
func testSphinxReplayPersistence(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Carol and Dave with Carol being
|
|
||||||
// the sole funder of the channel.
|
|
||||||
chanAmt := btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// First, we'll create Dave, the receiver, and start him in hodl mode.
|
|
||||||
dave := net.NewNode(t.t, "Dave", []string{"--hodl.exit-settle"})
|
|
||||||
|
|
||||||
// We must remember to shutdown the nodes we created for the duration
|
|
||||||
// of the tests, only leaving the two seed nodes (Alice and Bob) within
|
|
||||||
// our test network.
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
// Next, we'll create Carol and establish a channel to from her to
|
|
||||||
// Dave. Carol is started in both unsafe-replay which will cause her to
|
|
||||||
// replay any pending Adds held in memory upon reconnection.
|
|
||||||
carol := net.NewNode(t.t, "Carol", []string{"--unsafe-replay"})
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, carol, dave)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Next, we'll create Fred who is going to initiate the payment and
|
|
||||||
// establish a channel to from him to Carol. We can't perform this test
|
|
||||||
// by paying from Carol directly to Dave, because the '--unsafe-replay'
|
|
||||||
// setup doesn't apply to locally added htlcs. In that case, the
|
|
||||||
// mailbox, that is responsible for generating the replay, is bypassed.
|
|
||||||
fred := net.NewNode(t.t, "Fred", nil)
|
|
||||||
defer shutdownAndAssert(net, t, fred)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, fred, carol)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, fred)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointFC := openChannelAndAssert(
|
|
||||||
ctxt, t, net, fred, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now that the channel is open, create an invoice for Dave which
|
|
||||||
// expects a payment of 1000 satoshis from Carol paid via a particular
|
|
||||||
// preimage.
|
|
||||||
const paymentAmt = 1000
|
|
||||||
preimage := bytes.Repeat([]byte("A"), 32)
|
|
||||||
invoice := &lnrpc.Invoice{
|
|
||||||
Memo: "testing",
|
|
||||||
RPreimage: preimage,
|
|
||||||
Value: paymentAmt,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
invoiceResp, err := dave.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all channels to be recognized and advertized.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't advertise channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
err = dave.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't advertise channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointFC)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't advertise channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
err = fred.WaitForNetworkChannelOpen(ctxt, chanPointFC)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't advertise channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the invoice for Dave added, send a payment from Fred paying
|
|
||||||
// to the above generated invoice.
|
|
||||||
ctx, cancel := context.WithCancel(ctxb)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
payStream, err := fred.RouterClient.SendPaymentV2(
|
|
||||||
ctx,
|
|
||||||
&routerrpc.SendPaymentRequest{
|
|
||||||
PaymentRequest: invoiceResp.PaymentRequest,
|
|
||||||
TimeoutSeconds: 60,
|
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to open payment stream: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
|
|
||||||
// Dave's invoice should not be marked as settled.
|
|
||||||
payHash := &lnrpc.PaymentHash{
|
|
||||||
RHash: invoiceResp.RHash,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
dbInvoice, err := dave.LookupInvoice(ctxt, payHash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to lookup invoice: %v", err)
|
|
||||||
}
|
|
||||||
if dbInvoice.Settled {
|
|
||||||
t.Fatalf("dave's invoice should not be marked as settled: %v",
|
|
||||||
spew.Sdump(dbInvoice))
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the payment sent but hedl, all balance related stats should not
|
|
||||||
// have changed.
|
|
||||||
err = wait.InvariantNoError(
|
|
||||||
assertAmountSent(0, carol, dave), 3*time.Second,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the first payment sent, restart dave to make sure he is
|
|
||||||
// persisting the information required to detect replayed sphinx
|
|
||||||
// packets.
|
|
||||||
if err := net.RestartNode(dave, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart dave: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol should retransmit the Add hedl in her mailbox on startup. Dave
|
|
||||||
// should not accept the replayed Add, and actually fail back the
|
|
||||||
// pending payment. Even though he still holds the original settle, if
|
|
||||||
// he does fail, it is almost certainly caused by the sphinx replay
|
|
||||||
// protection, as it is the only validation we do in hodl mode.
|
|
||||||
result, err := getPaymentResult(payStream)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to receive payment response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that Fred receives the expected failure after Carol sent a
|
|
||||||
// duplicate packet that fails due to sphinx replay detection.
|
|
||||||
if result.Status == lnrpc.Payment_SUCCEEDED {
|
|
||||||
t.Fatalf("expected payment error")
|
|
||||||
}
|
|
||||||
assertLastHTLCError(t, fred, lnrpc.Failure_INVALID_ONION_KEY)
|
|
||||||
|
|
||||||
// Since the payment failed, the balance should still be left
|
|
||||||
// unaltered.
|
|
||||||
err = wait.InvariantNoError(
|
|
||||||
assertAmountSent(0, carol, dave), 3*time.Second,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPoint, true)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, carol, chanPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testListChannels checks that the response from ListChannels is correct. It
|
|
||||||
// tests the values in all ChannelConstraints are returned as expected. Once
|
|
||||||
// ListChannels becomes mature, a test against all fields in ListChannels should
|
|
||||||
// be performed.
|
|
||||||
func testListChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const aliceRemoteMaxHtlcs = 50
|
|
||||||
const bobRemoteMaxHtlcs = 100
|
|
||||||
|
|
||||||
// Create two fresh nodes and open a channel between them.
|
|
||||||
alice := net.NewNode(t.t, "Alice", nil)
|
|
||||||
defer shutdownAndAssert(net, t, alice)
|
|
||||||
|
|
||||||
bob := net.NewNode(
|
|
||||||
t.t, "Bob", []string{
|
|
||||||
fmt.Sprintf(
|
|
||||||
"--default-remote-max-htlcs=%v",
|
|
||||||
bobRemoteMaxHtlcs,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
defer shutdownAndAssert(net, t, bob)
|
|
||||||
|
|
||||||
// Connect Alice to Bob.
|
|
||||||
net.ConnectNodes(ctxb, t.t, alice, bob)
|
|
||||||
|
|
||||||
// Give Alice some coins so she can fund a channel.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, alice)
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
|
||||||
// being the sole funder of the channel. The minial HTLC amount is set to
|
|
||||||
// 4200 msats.
|
|
||||||
const customizedMinHtlc = 4200
|
|
||||||
|
|
||||||
chanAmt := btcutil.Amount(100000)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, alice, bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
MinHtlc: customizedMinHtlc,
|
|
||||||
RemoteMaxHtlcs: aliceRemoteMaxHtlcs,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for Alice and Bob to receive the channel edge from the
|
|
||||||
// funding manager.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err := alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the alice->bob channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = bob.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't see the bob->alice channel before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice should have one channel opened with Bob.
|
|
||||||
assertNodeNumChannels(t, alice, 1)
|
|
||||||
// Bob should have one channel opened with Alice.
|
|
||||||
assertNodeNumChannels(t, bob, 1)
|
|
||||||
|
|
||||||
// Get the ListChannel response from Alice.
|
|
||||||
listReq := &lnrpc.ListChannelsRequest{}
|
|
||||||
ctxb = context.Background()
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := alice.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for %s's channel list: %v",
|
|
||||||
alice.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the returned response is correct.
|
|
||||||
aliceChannel := resp.Channels[0]
|
|
||||||
|
|
||||||
// defaultConstraints is a ChannelConstraints with default values. It is
|
|
||||||
// used to test against Alice's local channel constraints.
|
|
||||||
defaultConstraints := &lnrpc.ChannelConstraints{
|
|
||||||
CsvDelay: 4,
|
|
||||||
ChanReserveSat: 1000,
|
|
||||||
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
|
|
||||||
MaxPendingAmtMsat: 99000000,
|
|
||||||
MinHtlcMsat: 1,
|
|
||||||
MaxAcceptedHtlcs: bobRemoteMaxHtlcs,
|
|
||||||
}
|
|
||||||
assertChannelConstraintsEqual(
|
|
||||||
t, defaultConstraints, aliceChannel.LocalConstraints,
|
|
||||||
)
|
|
||||||
|
|
||||||
// customizedConstraints is a ChannelConstraints with customized values.
|
|
||||||
// Ideally, all these values can be passed in when creating the channel.
|
|
||||||
// Currently, only the MinHtlcMsat is customized. It is used to check
|
|
||||||
// against Alice's remote channel constratins.
|
|
||||||
customizedConstraints := &lnrpc.ChannelConstraints{
|
|
||||||
CsvDelay: 4,
|
|
||||||
ChanReserveSat: 1000,
|
|
||||||
DustLimitSat: uint64(lnwallet.DefaultDustLimit()),
|
|
||||||
MaxPendingAmtMsat: 99000000,
|
|
||||||
MinHtlcMsat: customizedMinHtlc,
|
|
||||||
MaxAcceptedHtlcs: aliceRemoteMaxHtlcs,
|
|
||||||
}
|
|
||||||
assertChannelConstraintsEqual(
|
|
||||||
t, customizedConstraints, aliceChannel.RemoteConstraints,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the ListChannel response for Bob.
|
|
||||||
listReq = &lnrpc.ListChannelsRequest{}
|
|
||||||
ctxb = context.Background()
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err = bob.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for %s's channel "+
|
|
||||||
"list: %v", bob.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bobChannel := resp.Channels[0]
|
|
||||||
if bobChannel.ChannelPoint != aliceChannel.ChannelPoint {
|
|
||||||
t.Fatalf("Bob's channel point mismatched, want: %s, got: %s",
|
|
||||||
chanPoint.String(), bobChannel.ChannelPoint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check channel constraints match. Alice's local channel constraint should
|
|
||||||
// be equal to Bob's remote channel constraint, and her remote one should
|
|
||||||
// be equal to Bob's local one.
|
|
||||||
assertChannelConstraintsEqual(
|
|
||||||
t, aliceChannel.LocalConstraints, bobChannel.RemoteConstraints,
|
|
||||||
)
|
|
||||||
assertChannelConstraintsEqual(
|
|
||||||
t, aliceChannel.RemoteConstraints, bobChannel.LocalConstraints,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMaxPendingChannels checks that error is returned from remote peer if
|
|
||||||
// max pending channel number was exceeded and that '--maxpendingchannels' flag
|
|
||||||
// exists and works properly.
|
|
||||||
func testMaxPendingChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
maxPendingChannels := lncfg.DefaultMaxPendingChannels + 1
|
|
||||||
amount := funding.MaxBtcFundingAmount
|
|
||||||
|
|
||||||
// Create a new node (Carol) with greater number of max pending
|
|
||||||
// channels.
|
|
||||||
args := []string{
|
|
||||||
fmt.Sprintf("--maxpendingchannels=%v", maxPendingChannels),
|
|
||||||
}
|
|
||||||
carol := net.NewNode(t.t, "Carol", args)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalance := btcutil.Amount(maxPendingChannels) * amount
|
|
||||||
net.SendCoins(ctxt, t.t, carolBalance, carol)
|
|
||||||
|
|
||||||
// Send open channel requests without generating new blocks thereby
|
|
||||||
// increasing pool of pending channels. Then check that we can't open
|
|
||||||
// the channel if the number of pending channels exceed max value.
|
|
||||||
openStreams := make([]lnrpc.Lightning_OpenChannelClient, maxPendingChannels)
|
|
||||||
for i := 0; i < maxPendingChannels; i++ {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
stream := openChannelStream(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
openStreams[i] = stream
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol exhausted available amount of pending channels, next open
|
|
||||||
// channel request should cause ErrorGeneric to be sent back to Alice.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
_, err := net.OpenChannel(
|
|
||||||
ctxt, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("error wasn't received")
|
|
||||||
} else if !strings.Contains(
|
|
||||||
err.Error(), lnwire.ErrMaxPendingChannels.Error(),
|
|
||||||
) {
|
|
||||||
t.Fatalf("not expected error was received: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now our channels are in pending state, in order to not interfere
|
|
||||||
// with other tests we should clean up - complete opening of the
|
|
||||||
// channel and then close it.
|
|
||||||
|
|
||||||
// Mine 6 blocks, then wait for node's to notify us that the channel has
|
|
||||||
// been opened. The funding transactions should be found within the
|
|
||||||
// first newly mined block. 6 blocks make sure the funding transaction
|
|
||||||
// has enough confirmations to be announced publicly.
|
|
||||||
block := mineBlocks(t, net, 6, maxPendingChannels)[0]
|
|
||||||
|
|
||||||
chanPoints := make([]*lnrpc.ChannelPoint, maxPendingChannels)
|
|
||||||
for i, stream := range openStreams {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
fundingChanPoint, err := net.WaitForChannelOpen(ctxt, stream)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error while waiting for channel open: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fundingTxID, err := lnrpc.GetChanPointFundingTxid(fundingChanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the funding transaction enters a block, and is
|
|
||||||
// properly advertised by Alice.
|
|
||||||
assertTxInBlock(t, block, fundingTxID)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, fundingChanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("channel not seen on network before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The channel should be listed in the peer information
|
|
||||||
// returned by both peers.
|
|
||||||
chanPoint := wire.OutPoint{
|
|
||||||
Hash: *fundingTxID,
|
|
||||||
Index: fundingChanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.AssertChannelExists(ctxt, net.Alice, &chanPoint); err != nil {
|
|
||||||
t.Fatalf("unable to assert channel existence: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chanPoints[i] = fundingChanPoint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, close the channel between Alice and Carol, asserting that the
|
|
||||||
// channel has been properly closed on-chain.
|
|
||||||
for _, chanPoint := range chanPoints {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testGarbageCollectLinkNodes tests that we properly garbase collect link nodes
|
|
||||||
// from the database and the set of persistent connections within the server.
|
|
||||||
func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const (
|
|
||||||
chanAmt = 1000000
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open a channel between Alice and Bob which will later be
|
|
||||||
// cooperatively closed.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
coopChanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create Carol's node and connect Alice to her.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
|
||||||
|
|
||||||
// Open a channel between Alice and Carol which will later be force
|
|
||||||
// closed.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
forceCloseChanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now, create Dave's a node and also open a channel between Alice and
|
|
||||||
// him. This link will serve as the only persistent link throughout
|
|
||||||
// restarts in this test.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Alice, dave)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
persistentChanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// isConnected is a helper closure that checks if a peer is connected to
|
|
||||||
// Alice.
|
|
||||||
isConnected := func(pubKey string) bool {
|
|
||||||
req := &lnrpc.ListPeersRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := net.Alice.ListPeers(ctxt, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to retrieve alice's peers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, peer := range resp.Peers {
|
|
||||||
if peer.PubKey == pubKey {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart both Bob and Carol to ensure Alice is able to reconnect to
|
|
||||||
// them.
|
|
||||||
if err := net.RestartNode(net.Bob, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart bob's node: %v", err)
|
|
||||||
}
|
|
||||||
if err := net.RestartNode(carol, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart carol's node: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Eventually(t.t, func() bool {
|
|
||||||
return isConnected(net.Bob.PubKeyStr)
|
|
||||||
}, defaultTimeout, 20*time.Millisecond)
|
|
||||||
require.Eventually(t.t, func() bool {
|
|
||||||
return isConnected(carol.PubKeyStr)
|
|
||||||
}, defaultTimeout, 20*time.Millisecond)
|
|
||||||
|
|
||||||
// We'll also restart Alice to ensure she can reconnect to her peers
|
|
||||||
// with open channels.
|
|
||||||
if err := net.RestartNode(net.Alice, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart alice's node: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Eventually(t.t, func() bool {
|
|
||||||
return isConnected(net.Bob.PubKeyStr)
|
|
||||||
}, defaultTimeout, 20*time.Millisecond)
|
|
||||||
require.Eventually(t.t, func() bool {
|
|
||||||
return isConnected(carol.PubKeyStr)
|
|
||||||
}, defaultTimeout, 20*time.Millisecond)
|
|
||||||
require.Eventually(t.t, func() bool {
|
|
||||||
return isConnected(dave.PubKeyStr)
|
|
||||||
}, defaultTimeout, 20*time.Millisecond)
|
|
||||||
err := wait.Predicate(func() bool {
|
|
||||||
return isConnected(dave.PubKeyStr)
|
|
||||||
}, defaultTimeout)
|
|
||||||
|
|
||||||
// testReconnection is a helper closure that restarts the nodes at both
|
|
||||||
// ends of a channel to ensure they do not reconnect after restarting.
|
|
||||||
// When restarting Alice, we'll first need to ensure she has
|
|
||||||
// reestablished her connection with Dave, as they still have an open
|
|
||||||
// channel together.
|
|
||||||
testReconnection := func(node *lntest.HarnessNode) {
|
|
||||||
// Restart both nodes, to trigger the pruning logic.
|
|
||||||
if err := net.RestartNode(node, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart %v's node: %v",
|
|
||||||
node.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := net.RestartNode(net.Alice, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart alice's node: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now restart both nodes and make sure they don't reconnect.
|
|
||||||
if err := net.RestartNode(node, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart %v's node: %v", node.Name(),
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
err = wait.Invariant(func() bool {
|
|
||||||
return !isConnected(node.PubKeyStr)
|
|
||||||
}, 5*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice reconnected to %v", node.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := net.RestartNode(net.Alice, nil); err != nil {
|
|
||||||
t.Fatalf("unable to restart alice's node: %v", err)
|
|
||||||
}
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
return isConnected(dave.PubKeyStr)
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't reconnect to Dave")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wait.Invariant(func() bool {
|
|
||||||
return !isConnected(node.PubKeyStr)
|
|
||||||
}, 5*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice reconnected to %v", node.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, we'll close the channel between Alice and Bob and ensure there
|
|
||||||
// is no reconnection logic between the both once the channel is fully
|
|
||||||
// closed.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, coopChanPoint, false)
|
|
||||||
|
|
||||||
testReconnection(net.Bob)
|
|
||||||
|
|
||||||
// We'll do the same with Alice and Carol, but this time we'll force
|
|
||||||
// close the channel instead.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, forceCloseChanPoint, true)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, net.Alice, forceCloseChanPoint)
|
|
||||||
|
|
||||||
// We'll need to mine some blocks in order to mark the channel fully
|
|
||||||
// closed.
|
|
||||||
_, err = net.Miner.Client.Generate(chainreg.DefaultBitcoinTimeLockDelta - defaultCSV)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate blocks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before we test reconnection, we'll ensure that the channel has been
|
|
||||||
// fully cleaned up for both Carol and Alice.
|
|
||||||
var predErr error
|
|
||||||
pendingChansRequest := &lnrpc.PendingChannelsRequest{}
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err := net.Alice.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
pendingChanResp, err = carol.PendingChannels(
|
|
||||||
ctxt, pendingChansRequest,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to query for pending "+
|
|
||||||
"channels: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
|
||||||
|
|
||||||
return predErr == nil
|
|
||||||
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("channels not marked as fully resolved: %v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
testReconnection(carol)
|
|
||||||
|
|
||||||
// Finally, we'll ensure that Bob and Carol no longer show in Alice's
|
|
||||||
// channel graph.
|
|
||||||
describeGraphReq := &lnrpc.ChannelGraphRequest{
|
|
||||||
IncludeUnannounced: true,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
channelGraph, err := net.Alice.DescribeGraph(ctxt, describeGraphReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to query for alice's channel graph: %v", err)
|
|
||||||
}
|
|
||||||
for _, node := range channelGraph.Nodes {
|
|
||||||
if node.PubKey == net.Bob.PubKeyStr {
|
|
||||||
t.Fatalf("did not expect to find bob in the channel " +
|
|
||||||
"graph, but did")
|
|
||||||
}
|
|
||||||
if node.PubKey == carol.PubKeyStr {
|
|
||||||
t.Fatalf("did not expect to find carol in the channel " +
|
|
||||||
"graph, but did")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the test is done, we can also close the persistent link.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, persistentChanPoint, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testDataLossProtection tests that if one of the nodes in a channel
|
|
||||||
// relationship lost state, they will detect this during channel sync, and the
|
|
||||||
// up-to-date party will force close the channel, giving the outdated party the
|
|
||||||
// opportunity to sweep its output.
|
|
||||||
func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
const (
|
|
||||||
chanAmt = funding.MaxBtcFundingAmount
|
|
||||||
paymentAmt = 10000
|
|
||||||
numInvoices = 6
|
|
||||||
)
|
|
||||||
|
|
||||||
// Carol will be the up-to-date party. We set --nolisten to ensure Dave
|
|
||||||
// won't be able to connect to her and trigger the channel data
|
|
||||||
// protection logic automatically. We also can't have Carol
|
|
||||||
// automatically re-connect too early, otherwise DLP would be initiated
|
|
||||||
// at the wrong moment.
|
|
||||||
carol := net.NewNode(
|
|
||||||
t.t, "Carol", []string{"--nolisten", "--minbackoff=1h"},
|
|
||||||
)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
// Dave will be the party losing his state.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
// Before we make a channel, we'll load up Carol with some coins sent
|
|
||||||
// directly from the miner.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
// timeTravel is a method that will make Carol open a channel to the
|
|
||||||
// passed node, settle a series of payments, then reset the node back
|
|
||||||
// to the state before the payments happened. When this method returns
|
|
||||||
// the node will be unaware of the new state updates. The returned
|
|
||||||
// function can be used to restart the node in this state.
|
|
||||||
timeTravel := func(node *lntest.HarnessNode) (func() error,
|
|
||||||
*lnrpc.ChannelPoint, int64, error) {
|
|
||||||
|
|
||||||
// We must let the node communicate with Carol before they are
|
|
||||||
// able to open channel, so we connect them.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.EnsureConnected(ctxt, t.t, carol, node)
|
|
||||||
|
|
||||||
// We'll first open up a channel between them with a 0.5 BTC
|
|
||||||
// value.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, node,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// With the channel open, we'll create a few invoices for the
|
|
||||||
// node that Carol will pay to in order to advance the state of
|
|
||||||
// the channel.
|
|
||||||
// TODO(halseth): have dangling HTLCs on the commitment, able to
|
|
||||||
// retrieve funds?
|
|
||||||
payReqs, _, _, err := createPayReqs(
|
|
||||||
node, paymentAmt, numInvoices,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for Carol to receive the channel edge from the funding
|
|
||||||
// manager.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("carol didn't see the carol->%s channel "+
|
|
||||||
"before timeout: %v", node.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send payments from Carol using 3 of the payment hashes
|
|
||||||
// generated above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, carol, carol.RouterClient,
|
|
||||||
payReqs[:numInvoices/2], true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next query for the node's channel state, as we sent 3
|
|
||||||
// payments of 10k satoshis each, it should now see his balance
|
|
||||||
// as being 30k satoshis.
|
|
||||||
var nodeChan *lnrpc.Channel
|
|
||||||
var predErr error
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
bChan, err := getChanInfo(ctxt, node)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get channel info: %v", err)
|
|
||||||
}
|
|
||||||
if bChan.LocalBalance != 30000 {
|
|
||||||
predErr = fmt.Errorf("balance is incorrect, "+
|
|
||||||
"got %v, expected %v",
|
|
||||||
bChan.LocalBalance, 30000)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeChan = bChan
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the current commitment height (update number), we'll
|
|
||||||
// later revert him to this state after additional updates to
|
|
||||||
// revoke this state.
|
|
||||||
stateNumPreCopy := nodeChan.NumUpdates
|
|
||||||
|
|
||||||
// With the temporary file created, copy the current state into
|
|
||||||
// the temporary file we created above. Later after more
|
|
||||||
// updates, we'll restore this state.
|
|
||||||
if err := net.BackupDb(node); err != nil {
|
|
||||||
t.Fatalf("unable to copy database files: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, send more payments from , using the remaining
|
|
||||||
// payment hashes.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, carol, carol.RouterClient,
|
|
||||||
payReqs[numInvoices/2:], true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
nodeChan, err = getChanInfo(ctxt, node)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get dave chan info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we shutdown the node, copying over the its temporary
|
|
||||||
// database state which has the *prior* channel state over his
|
|
||||||
// current most up to date state. With this, we essentially
|
|
||||||
// force the node to travel back in time within the channel's
|
|
||||||
// history.
|
|
||||||
if err = net.RestartNode(node, func() error {
|
|
||||||
return net.RestoreDb(node)
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("unable to restart node: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the channel is still there from the PoV of the
|
|
||||||
// node.
|
|
||||||
assertNodeNumChannels(t, node, 1)
|
|
||||||
|
|
||||||
// Now query for the channel state, it should show that it's at
|
|
||||||
// a state number in the past, not the *latest* state.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
nodeChan, err = getChanInfo(ctxt, node)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get dave chan info: %v", err)
|
|
||||||
}
|
|
||||||
if nodeChan.NumUpdates != stateNumPreCopy {
|
|
||||||
t.Fatalf("db copy failed: %v", nodeChan.NumUpdates)
|
|
||||||
}
|
|
||||||
|
|
||||||
balReq := &lnrpc.WalletBalanceRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
balResp, err := node.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get dave's balance: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
restart, err := net.SuspendNode(node)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to suspend node: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return restart, chanPoint, balResp.ConfirmedBalance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset Dave to a state where he has an outdated channel state.
|
|
||||||
restartDave, _, daveStartingBalance, err := timeTravel(dave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to time travel dave: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We make a note of the nodes' current on-chain balances, to make sure
|
|
||||||
// they are able to retrieve the channel funds eventually,
|
|
||||||
balReq := &lnrpc.WalletBalanceRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalResp, err := carol.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get carol's balance: %v", err)
|
|
||||||
}
|
|
||||||
carolStartingBalance := carolBalResp.ConfirmedBalance
|
|
||||||
|
|
||||||
// Restart Dave to trigger a channel resync.
|
|
||||||
if err := restartDave(); err != nil {
|
|
||||||
t.Fatalf("unable to restart dave: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that once Dave comes up, they reconnect, Carol force closes
|
|
||||||
// on chain, and both of them properly carry out the DLP protocol.
|
|
||||||
assertDLPExecuted(
|
|
||||||
net, t, carol, carolStartingBalance, dave, daveStartingBalance,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
// As a second part of this test, we will test the scenario where a
|
|
||||||
// channel is closed while Dave is offline, loses his state and comes
|
|
||||||
// back online. In this case the node should attempt to resync the
|
|
||||||
// channel, and the peer should resend a channel sync message for the
|
|
||||||
// closed channel, such that Dave can retrieve his funds.
|
|
||||||
//
|
|
||||||
// We start by letting Dave time travel back to an outdated state.
|
|
||||||
restartDave, chanPoint2, daveStartingBalance, err := timeTravel(dave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to time travel eve: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get carol's balance: %v", err)
|
|
||||||
}
|
|
||||||
carolStartingBalance = carolBalResp.ConfirmedBalance
|
|
||||||
|
|
||||||
// Now let Carol force close the channel while Dave is offline.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPoint2, true)
|
|
||||||
|
|
||||||
// Wait for the channel to be marked pending force close.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = waitForChannelPendingForceClose(ctxt, carol, chanPoint2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("channel not pending force close: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine enough blocks for Carol to sweep her funds.
|
|
||||||
mineBlocks(t, net, defaultCSV-1, 0)
|
|
||||||
|
|
||||||
carolSweep, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to find Carol's sweep tx in mempool: %v", err)
|
|
||||||
}
|
|
||||||
block := mineBlocks(t, net, 1, 1)[0]
|
|
||||||
assertTxInBlock(t, block, carolSweep)
|
|
||||||
|
|
||||||
// Now the channel should be fully closed also from Carol's POV.
|
|
||||||
assertNumPendingChannels(t, carol, 0, 0)
|
|
||||||
|
|
||||||
// Make sure Carol got her balance back.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
carolBalResp, err = carol.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get carol's balance: %v", err)
|
|
||||||
}
|
|
||||||
carolBalance := carolBalResp.ConfirmedBalance
|
|
||||||
if carolBalance <= carolStartingBalance {
|
|
||||||
t.Fatalf("expected carol to have balance above %d, "+
|
|
||||||
"instead had %v", carolStartingBalance,
|
|
||||||
carolBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNodeNumChannels(t, carol, 0)
|
|
||||||
|
|
||||||
// When Dave comes online, he will reconnect to Carol, try to resync
|
|
||||||
// the channel, but it will already be closed. Carol should resend the
|
|
||||||
// information Dave needs to sweep his funds.
|
|
||||||
if err := restartDave(); err != nil {
|
|
||||||
t.Fatalf("unable to restart Eve: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dave should sweep his funds.
|
|
||||||
_, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to find Dave's sweep tx in mempool: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mine a block to confirm the sweep, and make sure Dave got his
|
|
||||||
// balance back.
|
|
||||||
mineBlocks(t, net, 1, 1)
|
|
||||||
assertNodeNumChannels(t, dave, 0)
|
|
||||||
|
|
||||||
err = wait.NoError(func() error {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
daveBalResp, err := dave.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get dave's balance: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
|
|
||||||
daveBalance := daveBalResp.ConfirmedBalance
|
|
||||||
if daveBalance <= daveStartingBalance {
|
|
||||||
return fmt.Errorf("expected dave to have balance "+
|
|
||||||
"above %d, intead had %v", daveStartingBalance,
|
|
||||||
daveBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRejectHTLC tests that a node can be created with the flag --rejecthtlc.
|
|
||||||
// This means that the node will reject all forwarded HTLCs but can still
|
|
||||||
// accept direct HTLCs as well as send HTLCs.
|
|
||||||
func testRejectHTLC(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
// RejectHTLC
|
|
||||||
// Alice ------> Carol ------> Bob
|
|
||||||
//
|
|
||||||
const chanAmt = btcutil.Amount(1000000)
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// Create Carol with reject htlc flag.
|
|
||||||
carol := net.NewNode(t.t, "Carol", []string{"--rejecthtlc"})
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
// Connect Alice to Carol.
|
|
||||||
net.ConnectNodes(ctxb, t.t, net.Alice, carol)
|
|
||||||
|
|
||||||
// Connect Carol to Bob.
|
|
||||||
net.ConnectNodes(ctxb, t.t, carol, net.Bob)
|
|
||||||
|
|
||||||
// Send coins to Carol.
|
|
||||||
net.SendCoins(ctxb, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
// Send coins to Alice.
|
|
||||||
net.SendCoins(ctxb, t.t, btcutil.SatoshiPerBitcent, net.Alice)
|
|
||||||
|
|
||||||
// Open a channel between Alice and Carol.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open a channel between Carol and Bob.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Channel should be ready for payments.
|
|
||||||
const payAmt = 100
|
|
||||||
|
|
||||||
// Helper closure to generate a random pre image.
|
|
||||||
genPreImage := func() []byte {
|
|
||||||
preimage := make([]byte, 32)
|
|
||||||
|
|
||||||
_, err := rand.Read(preimage)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate preimage: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return preimage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an invoice from Carol of 100 satoshis.
|
|
||||||
// We expect Alice to be able to pay this invoice.
|
|
||||||
preimage := genPreImage()
|
|
||||||
|
|
||||||
carolInvoice := &lnrpc.Invoice{
|
|
||||||
Memo: "testing - alice should pay carol",
|
|
||||||
RPreimage: preimage,
|
|
||||||
Value: payAmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol adds the invoice to her database.
|
|
||||||
resp, err := carol.AddInvoice(ctxb, carolInvoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice pays Carols invoice.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, net.Alice, net.Alice.RouterClient,
|
|
||||||
[]string{resp.PaymentRequest}, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments from alice to carol: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an invoice from Bob of 100 satoshis.
|
|
||||||
// We expect Carol to be able to pay this invoice.
|
|
||||||
preimage = genPreImage()
|
|
||||||
|
|
||||||
bobInvoice := &lnrpc.Invoice{
|
|
||||||
Memo: "testing - carol should pay bob",
|
|
||||||
RPreimage: preimage,
|
|
||||||
Value: payAmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bob adds the invoice to his database.
|
|
||||||
resp, err = net.Bob.AddInvoice(ctxb, bobInvoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carol pays Bobs invoice.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, carol, carol.RouterClient,
|
|
||||||
[]string{resp.PaymentRequest}, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments from carol to bob: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an invoice from Bob of 100 satoshis.
|
|
||||||
// Alice attempts to pay Bob but this should fail, since we are
|
|
||||||
// using Carol as a hop and her node will reject onward HTLCs.
|
|
||||||
preimage = genPreImage()
|
|
||||||
|
|
||||||
bobInvoice = &lnrpc.Invoice{
|
|
||||||
Memo: "testing - alice tries to pay bob",
|
|
||||||
RPreimage: preimage,
|
|
||||||
Value: payAmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bob adds the invoice to his database.
|
|
||||||
resp, err = net.Bob.AddInvoice(ctxb, bobInvoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alice attempts to pay Bobs invoice. This payment should be rejected since
|
|
||||||
// we are using Carol as an intermediary hop, Carol is running lnd with
|
|
||||||
// --rejecthtlc.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, net.Alice, net.Alice.RouterClient,
|
|
||||||
[]string{resp.PaymentRequest}, true,
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf(
|
|
||||||
"should have been rejected, carol will not accept forwarded htlcs",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertLastHTLCError(t, net.Alice, lnrpc.Failure_CHANNEL_DISABLED)
|
|
||||||
|
|
||||||
// Close all channels.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNodeSignVerify(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
chanAmt := funding.MaxBtcFundingAmount
|
|
||||||
pushAmt := btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// Create a channel between alice and bob.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
aliceBobCh := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: pushAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
aliceMsg := []byte("alice msg")
|
|
||||||
|
|
||||||
// alice signs "alice msg" and sends her signature to bob.
|
|
||||||
sigReq := &lnrpc.SignMessageRequest{Msg: aliceMsg}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
sigResp, err := net.Alice.SignMessage(ctxt, sigReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SignMessage rpc call failed: %v", err)
|
|
||||||
}
|
|
||||||
aliceSig := sigResp.Signature
|
|
||||||
|
|
||||||
// bob verifying alice's signature should succeed since alice and bob are
|
|
||||||
// connected.
|
|
||||||
verifyReq := &lnrpc.VerifyMessageRequest{Msg: aliceMsg, Signature: aliceSig}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
verifyResp, err := net.Bob.VerifyMessage(ctxt, verifyReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("VerifyMessage failed: %v", err)
|
|
||||||
}
|
|
||||||
if !verifyResp.Valid {
|
|
||||||
t.Fatalf("alice's signature didn't validate")
|
|
||||||
}
|
|
||||||
if verifyResp.Pubkey != net.Alice.PubKeyStr {
|
|
||||||
t.Fatalf("alice's signature doesn't contain alice's pubkey.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// carol is a new node that is unconnected to alice or bob.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
carolMsg := []byte("carol msg")
|
|
||||||
|
|
||||||
// carol signs "carol msg" and sends her signature to bob.
|
|
||||||
sigReq = &lnrpc.SignMessageRequest{Msg: carolMsg}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
sigResp, err = carol.SignMessage(ctxt, sigReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SignMessage rpc call failed: %v", err)
|
|
||||||
}
|
|
||||||
carolSig := sigResp.Signature
|
|
||||||
|
|
||||||
// bob verifying carol's signature should fail since they are not connected.
|
|
||||||
verifyReq = &lnrpc.VerifyMessageRequest{Msg: carolMsg, Signature: carolSig}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
verifyResp, err = net.Bob.VerifyMessage(ctxt, verifyReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("VerifyMessage failed: %v", err)
|
|
||||||
}
|
|
||||||
if verifyResp.Valid {
|
|
||||||
t.Fatalf("carol's signature should not be valid")
|
|
||||||
}
|
|
||||||
if verifyResp.Pubkey != carol.PubKeyStr {
|
|
||||||
t.Fatalf("carol's signature doesn't contain her pubkey")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the channel between alice and bob.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, aliceBobCh, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSendUpdateDisableChannel ensures that a channel update with the disable
|
|
||||||
// flag set is sent once a channel has been either unilaterally or cooperatively
|
|
||||||
// closed.
|
|
||||||
func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const (
|
|
||||||
chanAmt = 100000
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open a channel between Alice and Bob and Alice and Carol. These will
|
|
||||||
// be closed later on in order to trigger channel update messages
|
|
||||||
// marking the channels as disabled.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAliceBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
carol := net.NewNode(
|
|
||||||
t.t, "Carol", []string{
|
|
||||||
"--minbackoff=10s",
|
|
||||||
"--chan-enable-timeout=1.5s",
|
|
||||||
"--chan-disable-timeout=3s",
|
|
||||||
"--chan-status-sample-interval=.5s",
|
|
||||||
})
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Alice, carol)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAliceCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// We create a new node Eve that has an inactive channel timeout of
|
|
||||||
// just 2 seconds (down from the default 20m). It will be used to test
|
|
||||||
// channel updates for channels going inactive.
|
|
||||||
eve := net.NewNode(
|
|
||||||
t.t, "Eve", []string{
|
|
||||||
"--minbackoff=10s",
|
|
||||||
"--chan-enable-timeout=1.5s",
|
|
||||||
"--chan-disable-timeout=3s",
|
|
||||||
"--chan-status-sample-interval=.5s",
|
|
||||||
})
|
|
||||||
defer shutdownAndAssert(net, t, eve)
|
|
||||||
|
|
||||||
// Give Eve some coins.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, eve)
|
|
||||||
|
|
||||||
// Connect Eve to Carol and Bob, and open a channel to carol.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, eve, carol)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, eve, net.Bob)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointEveCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, eve, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Launch a node for Dave which will connect to Bob in order to receive
|
|
||||||
// graph updates from. This will ensure that the channel updates are
|
|
||||||
// propagated throughout the network.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.ConnectNodes(ctxt, t.t, net.Bob, dave)
|
|
||||||
|
|
||||||
daveSub := subscribeGraphNotifications(ctxb, t, dave)
|
|
||||||
defer close(daveSub.quit)
|
|
||||||
|
|
||||||
// We should expect to see a channel update with the default routing
|
|
||||||
// policy, except that it should indicate the channel is disabled.
|
|
||||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
|
||||||
FeeBaseMsat: int64(chainreg.DefaultBitcoinBaseFeeMSat),
|
|
||||||
FeeRateMilliMsat: int64(chainreg.DefaultBitcoinFeeRate),
|
|
||||||
TimeLockDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
|
||||||
MinHtlc: 1000, // default value
|
|
||||||
MaxHtlcMsat: calculateMaxHtlc(chanAmt),
|
|
||||||
Disabled: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let Carol go offline. Since Eve has an inactive timeout of 2s, we
|
|
||||||
// expect her to send an update disabling the channel.
|
|
||||||
restartCarol, err := net.SuspendNode(carol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to suspend carol: %v", err)
|
|
||||||
}
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// We restart Carol. Since the channel now becomes active again, Eve
|
|
||||||
// should send a ChannelUpdate setting the channel no longer disabled.
|
|
||||||
if err := restartCarol(); err != nil {
|
|
||||||
t.Fatalf("unable to restart carol: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedPolicy.Disabled = false
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now we'll test a long disconnection. Disconnect Carol and Eve and
|
|
||||||
// ensure they both detect each other as disabled. Their min backoffs
|
|
||||||
// are high enough to not interfere with disabling logic.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.DisconnectNodes(ctxt, carol, eve); err != nil {
|
|
||||||
t.Fatalf("unable to disconnect Carol from Eve: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a disable from both Carol and Eve to come through.
|
|
||||||
expectedPolicy.Disabled = true
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
{carol.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reconnect Carol and Eve, this should cause them to reenable the
|
|
||||||
// channel from both ends after a short delay.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.EnsureConnected(ctxt, t.t, carol, eve)
|
|
||||||
|
|
||||||
expectedPolicy.Disabled = false
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
{carol.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now we'll test a short disconnection. Disconnect Carol and Eve, then
|
|
||||||
// reconnect them after one second so that their scheduled disables are
|
|
||||||
// aborted. One second is twice the status sample interval, so this
|
|
||||||
// should allow for the disconnect to be detected, but still leave time
|
|
||||||
// to cancel the announcement before the 3 second inactive timeout is
|
|
||||||
// hit.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.DisconnectNodes(ctxt, carol, eve); err != nil {
|
|
||||||
t.Fatalf("unable to disconnect Carol from Eve: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.EnsureConnected(ctxt, t.t, eve, carol)
|
|
||||||
|
|
||||||
// Since the disable should have been canceled by both Carol and Eve, we
|
|
||||||
// expect no channel updates to appear on the network.
|
|
||||||
assertNoChannelUpdates(t, daveSub, 4*time.Second)
|
|
||||||
|
|
||||||
// Close Alice's channels with Bob and Carol cooperatively and
|
|
||||||
// unilaterally respectively.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
_, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceBob, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to close channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
_, _, err = net.CloseChannel(ctxt, net.Alice, chanPointAliceCarol, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to close channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel close processes have been started, we should
|
|
||||||
// receive an update marking each as disabled.
|
|
||||||
expectedPolicy.Disabled = true
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{net.Alice.PubKeyStr, expectedPolicy, chanPointAliceBob},
|
|
||||||
{net.Alice.PubKeyStr, expectedPolicy, chanPointAliceCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Finally, close the channels by mining the closing transactions.
|
|
||||||
mineBlocks(t, net, 1, 2)
|
|
||||||
|
|
||||||
// Also do this check for Eve's channel with Carol.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
_, _, err = net.CloseChannel(ctxt, eve, chanPointEveCarol, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to close channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, daveSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{eve.PubKeyStr, expectedPolicy, chanPointEveCarol},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
mineBlocks(t, net, 1, 1)
|
|
||||||
|
|
||||||
// And finally, clean up the force closed channel by mining the
|
|
||||||
// sweeping transaction.
|
|
||||||
cleanupForceClose(t, net, net.Alice, chanPointAliceCarol)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testAbandonChannel abandones a channel and asserts that it is no
|
|
||||||
// longer open and not in one of the pending closure states. It also
|
|
||||||
// verifies that the abandoned channel is reported as closed with close
|
|
||||||
// type 'abandoned'.
|
|
||||||
func testAbandonChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// First establish a channel between Alice and Bob.
|
|
||||||
channelParam := lntest.OpenChannelParams{
|
|
||||||
Amt: funding.MaxBtcFundingAmount,
|
|
||||||
PushAmt: btcutil.Amount(100000),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPoint := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob, channelParam,
|
|
||||||
)
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
chanPointStr := fmt.Sprintf("%v:%v", txid, chanPoint.OutputIndex)
|
|
||||||
|
|
||||||
// Wait for channel to be confirmed open.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't report channel: %v", err)
|
|
||||||
}
|
|
||||||
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't report channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channel is open, we'll obtain its channel ID real quick
|
|
||||||
// so we can use it to query the graph below.
|
|
||||||
listReq := &lnrpc.ListChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceChannelList, err := net.Alice.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to fetch alice's channels: %v", err)
|
|
||||||
}
|
|
||||||
var chanID uint64
|
|
||||||
for _, channel := range aliceChannelList.Channels {
|
|
||||||
if channel.ChannelPoint == chanPointStr {
|
|
||||||
chanID = channel.ChanId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if chanID == 0 {
|
|
||||||
t.Fatalf("unable to find channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// To make sure the channel is removed from the backup file as well when
|
|
||||||
// being abandoned, grab a backup snapshot so we can compare it with the
|
|
||||||
// later state.
|
|
||||||
bkupBefore, err := ioutil.ReadFile(net.Alice.ChanBackupPath())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not get channel backup before abandoning "+
|
|
||||||
"channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send request to abandon channel.
|
|
||||||
abandonChannelRequest := &lnrpc.AbandonChannelRequest{
|
|
||||||
ChannelPoint: chanPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
_, err = net.Alice.AbandonChannel(ctxt, abandonChannelRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to abandon channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that channel in no longer open.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceChannelList, err = net.Alice.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to list channels: %v", err)
|
|
||||||
}
|
|
||||||
if len(aliceChannelList.Channels) != 0 {
|
|
||||||
t.Fatalf("alice should only have no channels open, "+
|
|
||||||
"instead she has %v",
|
|
||||||
len(aliceChannelList.Channels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that channel is not pending closure.
|
|
||||||
pendingReq := &lnrpc.PendingChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
alicePendingList, err := net.Alice.PendingChannels(ctxt, pendingReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to list pending channels: %v", err)
|
|
||||||
}
|
|
||||||
if len(alicePendingList.PendingClosingChannels) != 0 { //nolint:staticcheck
|
|
||||||
t.Fatalf("alice should only have no pending closing channels, "+
|
|
||||||
"instead she has %v",
|
|
||||||
len(alicePendingList.PendingClosingChannels)) //nolint:staticcheck
|
|
||||||
}
|
|
||||||
if len(alicePendingList.PendingForceClosingChannels) != 0 {
|
|
||||||
t.Fatalf("alice should only have no pending force closing "+
|
|
||||||
"channels instead she has %v",
|
|
||||||
len(alicePendingList.PendingForceClosingChannels))
|
|
||||||
}
|
|
||||||
if len(alicePendingList.WaitingCloseChannels) != 0 {
|
|
||||||
t.Fatalf("alice should only have no waiting close "+
|
|
||||||
"channels instead she has %v",
|
|
||||||
len(alicePendingList.WaitingCloseChannels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that channel is listed as abandoned.
|
|
||||||
closedReq := &lnrpc.ClosedChannelsRequest{
|
|
||||||
Abandoned: true,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceClosedList, err := net.Alice.ClosedChannels(ctxt, closedReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to list closed channels: %v", err)
|
|
||||||
}
|
|
||||||
if len(aliceClosedList.Channels) != 1 {
|
|
||||||
t.Fatalf("alice should only have a single abandoned channel, "+
|
|
||||||
"instead she has %v",
|
|
||||||
len(aliceClosedList.Channels))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the channel can no longer be found in the channel graph.
|
|
||||||
_, err = net.Alice.GetChanInfo(ctxb, &lnrpc.ChanInfoRequest{
|
|
||||||
ChanId: chanID,
|
|
||||||
})
|
|
||||||
if !strings.Contains(err.Error(), "marked as zombie") {
|
|
||||||
t.Fatalf("channel shouldn't be found in the channel " +
|
|
||||||
"graph!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the channel is no longer in the channel backup list.
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
bkupAfter, err := ioutil.ReadFile(net.Alice.ChanBackupPath())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not get channel backup before "+
|
|
||||||
"abandoning channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(bkupAfter) < len(bkupBefore)
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("channel wasn't removed from channel backup file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calling AbandonChannel again, should result in no new errors, as the
|
|
||||||
// channel has already been removed.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
_, err = net.Alice.AbandonChannel(ctxt, abandonChannelRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to abandon channel a second time: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we're done with the test, the channel can be closed. This
|
|
||||||
// is necessary to avoid unexpected outcomes of other tests that use
|
|
||||||
// Bob's lnd instance.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPoint, true)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, net.Bob, chanPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSweepAllCoins tests that we're able to properly sweep all coins from the
|
|
||||||
// wallet into a single target address at the specified fee rate.
|
|
||||||
func testSweepAllCoins(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// First, we'll make a new node, ainz who'll we'll use to test wallet
|
|
||||||
// sweeping.
|
|
||||||
ainz := net.NewNode(t.t, "Ainz", nil)
|
|
||||||
defer shutdownAndAssert(net, t, ainz)
|
|
||||||
|
|
||||||
// Next, we'll give Ainz exactly 2 utxos of 1 BTC each, with one of
|
|
||||||
// them being p2wkh and the other being a n2wpkh address.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, ainz)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoinsNP2WKH(ctxt, t.t, btcutil.SatoshiPerBitcoin, ainz)
|
|
||||||
|
|
||||||
// Ensure that we can't send coins to our own Pubkey.
|
|
||||||
info, err := ainz.GetInfo(ctxt, &lnrpc.GetInfoRequest{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get node info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a label that we will used to label the transaction with.
|
|
||||||
sendCoinsLabel := "send all coins"
|
|
||||||
|
|
||||||
sweepReq := &lnrpc.SendCoinsRequest{
|
|
||||||
Addr: info.IdentityPubkey,
|
|
||||||
SendAll: true,
|
|
||||||
Label: sendCoinsLabel,
|
|
||||||
}
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected SendCoins to users own pubkey to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that we can't send coins to another users Pubkey.
|
|
||||||
info, err = net.Alice.GetInfo(ctxt, &lnrpc.GetInfoRequest{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get node info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepReq = &lnrpc.SendCoinsRequest{
|
|
||||||
Addr: info.IdentityPubkey,
|
|
||||||
SendAll: true,
|
|
||||||
Label: sendCoinsLabel,
|
|
||||||
}
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected SendCoins to Alices pubkey to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the two coins above mined, we'll now instruct ainz to sweep all
|
|
||||||
// the coins to an external address not under its control.
|
|
||||||
// We will first attempt to send the coins to addresses that are not
|
|
||||||
// compatible with the current network. This is to test that the wallet
|
|
||||||
// will prevent any onchain transactions to addresses that are not on the
|
|
||||||
// same network as the user.
|
|
||||||
|
|
||||||
// Send coins to a testnet3 address.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
sweepReq = &lnrpc.SendCoinsRequest{
|
|
||||||
Addr: "tb1qfc8fusa98jx8uvnhzavxccqlzvg749tvjw82tg",
|
|
||||||
SendAll: true,
|
|
||||||
Label: sendCoinsLabel,
|
|
||||||
}
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected SendCoins to different network to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send coins to a mainnet address.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
sweepReq = &lnrpc.SendCoinsRequest{
|
|
||||||
Addr: "1MPaXKp5HhsLNjVSqaL7fChE3TVyrTMRT3",
|
|
||||||
SendAll: true,
|
|
||||||
Label: sendCoinsLabel,
|
|
||||||
}
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected SendCoins to different network to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send coins to a compatible address.
|
|
||||||
minerAddr, err := net.Miner.NewAddress()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create new miner addr: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepReq = &lnrpc.SendCoinsRequest{
|
|
||||||
Addr: minerAddr.String(),
|
|
||||||
SendAll: true,
|
|
||||||
Label: sendCoinsLabel,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to sweep coins: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll mine a block which should include the sweep transaction we
|
|
||||||
// generated above.
|
|
||||||
block := mineBlocks(t, net, 1, 1)[0]
|
|
||||||
|
|
||||||
// The sweep transaction should have exactly two inputs as we only had
|
|
||||||
// two UTXOs in the wallet.
|
|
||||||
sweepTx := block.Transactions[1]
|
|
||||||
if len(sweepTx.TxIn) != 2 {
|
|
||||||
t.Fatalf("expected 2 inputs instead have %v", len(sweepTx.TxIn))
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepTxStr := sweepTx.TxHash().String()
|
|
||||||
assertTxLabel(ctxb, t, ainz, sweepTxStr, sendCoinsLabel)
|
|
||||||
|
|
||||||
// While we are looking at labels, we test our label transaction command
|
|
||||||
// to make sure it is behaving as expected. First, we try to label our
|
|
||||||
// transaction with an empty label, and check that we fail as expected.
|
|
||||||
sweepHash := sweepTx.TxHash()
|
|
||||||
_, err = ainz.WalletKitClient.LabelTransaction(
|
|
||||||
ctxt, &walletrpc.LabelTransactionRequest{
|
|
||||||
Txid: sweepHash[:],
|
|
||||||
Label: "",
|
|
||||||
Overwrite: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for zero transaction label")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our error will be wrapped in a rpc error, so we check that it
|
|
||||||
// contains the error we expect.
|
|
||||||
errZeroLabel := "cannot label transaction with empty label"
|
|
||||||
if !strings.Contains(err.Error(), errZeroLabel) {
|
|
||||||
t.Fatalf("expected: zero label error, got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we try to relabel our transaction without setting the overwrite
|
|
||||||
// boolean. We expect this to fail, because the wallet requires setting
|
|
||||||
// of this param to prevent accidental overwrite of labels.
|
|
||||||
_, err = ainz.WalletKitClient.LabelTransaction(
|
|
||||||
ctxt, &walletrpc.LabelTransactionRequest{
|
|
||||||
Txid: sweepHash[:],
|
|
||||||
Label: "label that will not work",
|
|
||||||
Overwrite: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for tx already labelled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our error will be wrapped in a rpc error, so we check that it
|
|
||||||
// contains the error we expect.
|
|
||||||
if !strings.Contains(err.Error(), wallet.ErrTxLabelExists.Error()) {
|
|
||||||
t.Fatalf("expected: label exists, got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we overwrite our label with a new label, which should not
|
|
||||||
// fail.
|
|
||||||
newLabel := "new sweep tx label"
|
|
||||||
_, err = ainz.WalletKitClient.LabelTransaction(
|
|
||||||
ctxt, &walletrpc.LabelTransactionRequest{
|
|
||||||
Txid: sweepHash[:],
|
|
||||||
Label: newLabel,
|
|
||||||
Overwrite: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not label tx: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTxLabel(ctxb, t, ainz, sweepTxStr, newLabel)
|
|
||||||
|
|
||||||
// Finally, Ainz should now have no coins at all within his wallet.
|
|
||||||
balReq := &lnrpc.WalletBalanceRequest{}
|
|
||||||
resp, err := ainz.WalletBalance(ctxt, balReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get ainz's balance: %v", err)
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case resp.ConfirmedBalance != 0:
|
|
||||||
t.Fatalf("expected no confirmed balance, instead have %v",
|
|
||||||
resp.ConfirmedBalance)
|
|
||||||
|
|
||||||
case resp.UnconfirmedBalance != 0:
|
|
||||||
t.Fatalf("expected no unconfirmed balance, instead have %v",
|
|
||||||
resp.UnconfirmedBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we try again, but this time specifying an amount, then the call
|
|
||||||
// should fail.
|
|
||||||
sweepReq.Amount = 10000
|
|
||||||
_, err = ainz.SendCoins(ctxt, sweepReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("sweep attempt should fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
// TestLightningNetworkDaemon performs a series of integration tests amongst a
|
||||||
// programmatically driven network of lnd nodes.
|
// programmatically driven network of lnd nodes.
|
||||||
func TestLightningNetworkDaemon(t *testing.T) {
|
func TestLightningNetworkDaemon(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user