diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 3e53acf38..16c8abf18 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -5171,6 +5171,308 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) { } +// testUpdateChanStatus checks that calls to the UpdateChanStatus RPC update +// the channel graph as expected, and that channel state is properly updated +// in the presence of interleaved node disconnects / reconnects. +func testUpdateChanStatus(net *lntest.NetworkHarness, t *harnessTest) { + ctxb := context.Background() + + // Create two fresh nodes and open a channel between them. + alice, err := net.NewNode("Alice", []string{ + "--minbackoff=10s", + "--chan-enable-timeout=1.5s", + "--chan-disable-timeout=3s", + "--chan-status-sample-interval=.5s", + }) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, alice) + + bob, err := net.NewNode("Bob", []string{ + "--minbackoff=10s", + "--chan-enable-timeout=1.5s", + "--chan-disable-timeout=3s", + "--chan-status-sample-interval=.5s", + }) + if err != nil { + t.Fatalf("unable to create new node: %v", err) + } + defer shutdownAndAssert(net, t, bob) + + // Connect Alice to Bob. + if err := net.ConnectNodes(ctxb, alice, bob); err != nil { + t.Fatalf("unable to connect alice to bob: %v", err) + } + + // Give Alice some coins so she can fund a channel. + ctxt, _ := context.WithTimeout(ctxb, defaultTimeout) + err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, alice) + if err != nil { + t.Fatalf("unable to send coins to alice: %v", err) + } + + // Open a channel with 100k satoshis between Alice and Bob with Alice + // being the sole funder of the channel. + chanAmt := btcutil.Amount(100000) + ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout) + chanPoint := openChannelAndAssert( + ctxt, t, net, alice, bob, + lntest.OpenChannelParams{ + Amt: chanAmt, + }, + ) + + // 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) + } + + // Launch a node for Carol which will connect to Alice and Bob in + // order to receive graph updates. This will ensure that the + // channel updates are propagated throughout the network. + carol, err := net.NewNode("Carol", nil) + if err != nil { + t.Fatalf("unable to create Carol's node: %v", err) + } + defer shutdownAndAssert(net, t, carol) + + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.ConnectNodes(ctxt, alice, carol); err != nil { + t.Fatalf("unable to connect alice to carol: %v", err) + } + + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.ConnectNodes(ctxt, bob, carol); err != nil { + t.Fatalf("unable to connect bob to carol: %v", err) + } + + carolSub := subscribeGraphNotifications(t, ctxb, carol) + defer close(carolSub.quit) + + // sendReq sends an UpdateChanStatus request to the given node. + sendReq := func(node *lntest.HarnessNode, chanPoint *lnrpc.ChannelPoint, + action routerrpc.ChanStatusAction) { + + req := &routerrpc.UpdateChanStatusRequest{ + ChanPoint: chanPoint, + Action: action, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = node.RouterClient.UpdateChanStatus(ctxt, req) + if err != nil { + t.Fatalf("unable to call UpdateChanStatus for %s's node: %v", + node.Name(), err) + } + } + + // assertEdgeDisabled ensures that a given node has the correct + // Disabled state for a channel. + assertEdgeDisabled := func(node *lntest.HarnessNode, + chanPoint *lnrpc.ChannelPoint, disabled bool) { + + var predErr error + err = wait.Predicate(func() bool { + req := &lnrpc.ChannelGraphRequest{ + IncludeUnannounced: true, + } + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + chanGraph, err := node.DescribeGraph(ctxt, req) + if err != nil { + predErr = fmt.Errorf("unable to query node %v's graph: %v", node, err) + return false + } + numEdges := len(chanGraph.Edges) + if numEdges != 1 { + predErr = fmt.Errorf("expected to find 1 edge in the graph, found %d", numEdges) + return false + } + edge := chanGraph.Edges[0] + if edge.ChanPoint != chanPoint.GetFundingTxidStr() { + predErr = fmt.Errorf("expected chan_point %v, got %v", + chanPoint.GetFundingTxidStr(), edge.ChanPoint) + } + var policy *lnrpc.RoutingPolicy + if node.PubKeyStr == edge.Node1Pub { + policy = edge.Node1Policy + } else { + policy = edge.Node2Policy + } + if disabled != policy.Disabled { + predErr = fmt.Errorf("expected policy.Disabled to be %v, "+ + "but policy was %v", disabled, policy) + return false + } + return true + }, defaultTimeout) + if err != nil { + t.Fatalf("%v", predErr) + } + } + + // When updating the state of the channel between Alice and Bob, we + // should expect to see channel updates with the default routing + // policy. The value of "Disabled" will depend on the specific + // scenario being tested. + expectedPolicy := &lnrpc.RoutingPolicy{ + FeeBaseMsat: int64(chainreg.DefaultBitcoinBaseFeeMSat), + FeeRateMilliMsat: int64(chainreg.DefaultBitcoinFeeRate), + TimeLockDelta: chainreg.DefaultBitcoinTimeLockDelta, + MinHtlc: 1000, // default value + MaxHtlcMsat: calculateMaxHtlc(chanAmt), + } + + // Initially, the channel between Alice and Bob should not be + // disabled. + assertEdgeDisabled(alice, chanPoint, false) + + // Manually disable the channel and ensure that a "Disabled = true" + // update is propagated. + sendReq(alice, chanPoint, routerrpc.ChanStatusAction_DISABLE) + expectedPolicy.Disabled = true + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // Re-enable the channel and ensure that a "Disabled = false" update + // is propagated. + sendReq(alice, chanPoint, routerrpc.ChanStatusAction_ENABLE) + expectedPolicy.Disabled = false + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // Manually enabling a channel should NOT prevent subsequent + // disconnections from automatically disabling the channel again + // (we don't want to clutter the network with channels that are + // falsely advertised as enabled when they don't work). + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.DisconnectNodes(ctxt, alice, bob); err != nil { + t.Fatalf("unable to disconnect Alice from Bob: %v", err) + } + expectedPolicy.Disabled = true + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // Reconnecting the nodes should propagate a "Disabled = false" update. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.EnsureConnected(ctxt, alice, bob); err != nil { + t.Fatalf("unable to reconnect Alice to Bob: %v", err) + } + expectedPolicy.Disabled = false + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // Manually disabling the channel should prevent a subsequent + // disconnect / reconnect from re-enabling the channel on + // Alice's end. Note the asymmetry between manual enable and + // manual disable! + sendReq(alice, chanPoint, routerrpc.ChanStatusAction_DISABLE) + + // Alice sends out the "Disabled = true" update in response to + // the ChanStatusAction_DISABLE request. + expectedPolicy.Disabled = true + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.DisconnectNodes(ctxt, alice, bob); err != nil { + t.Fatalf("unable to disconnect Alice from Bob: %v", err) + } + + // Bob sends a "Disabled = true" update upon detecting the + // disconnect. + expectedPolicy.Disabled = true + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // Bob sends a "Disabled = false" update upon detecting the + // reconnect. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.EnsureConnected(ctxt, alice, bob); err != nil { + t.Fatalf("unable to reconnect Alice to Bob: %v", err) + } + expectedPolicy.Disabled = false + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // However, since we manually disabled the channel on Alice's end, + // the policy on Alice's end should still be "Disabled = true". Again, + // note the asymmetry between manual enable and manual disable! + assertEdgeDisabled(alice, chanPoint, true) + + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + if err := net.DisconnectNodes(ctxt, alice, bob); err != nil { + t.Fatalf("unable to disconnect Alice from Bob: %v", err) + } + + // Bob sends a "Disabled = true" update upon detecting the + // disconnect. + expectedPolicy.Disabled = true + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + + // After restoring automatic channel state management on Alice's end, + // BOTH Alice and Bob should set the channel state back to "enabled" + // on reconnect. + sendReq(alice, chanPoint, routerrpc.ChanStatusAction_AUTO) + if err := net.EnsureConnected(ctxt, alice, bob); err != nil { + t.Fatalf("unable to reconnect Alice to Bob: %v", err) + } + expectedPolicy.Disabled = false + waitForChannelUpdate( + t, carolSub, + []expectedChanUpdate{ + {alice.PubKeyStr, expectedPolicy, chanPoint}, + {bob.PubKeyStr, expectedPolicy, chanPoint}, + }, + ) + assertEdgeDisabled(alice, chanPoint, false) +} + func testListPayments(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 7f8404f74..17008187a 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -71,6 +71,10 @@ var allTestCases = []*testCase{ name: "list channels", test: testListChannels, }, + { + name: "update channel status", + test: testUpdateChanStatus, + }, { name: "list outgoing payments", test: testListPayments,