diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 42471e9c3..e4aed3142 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -206,6 +206,10 @@ var allTestCases = []*lntest.TestCase{ Name: "invoice update subscription", TestFunc: testInvoiceSubscriptions, }, + { + Name: "channel update subscription", + TestFunc: testChannelUpdateNotifications, + }, { Name: "streaming channel backup update", TestFunc: testChannelBackupUpdates, diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go index e6cdca4d8..7319660f9 100644 --- a/itest/lnd_open_channel_test.go +++ b/itest/lnd_open_channel_test.go @@ -3,6 +3,7 @@ package itest import ( "fmt" "strings" + "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -1342,3 +1343,101 @@ func testOpenChannelWithShutdownAddr(ht *lntest.HarnessTest) { require.Equal(ht, bobShutdownAddr.Address, bobUTXOConfirmed.Address) require.Equal(ht, paymentAmount, bobUTXOConfirmed.AmountSat) } + +// testChannelUpdateNotifications checks that clients subscribed to channel +// events receive real-time updates when the channel state changes. +func testChannelUpdateNotifications(ht *lntest.HarnessTest) { + // We'll start by creating two nodes, Alice and Bob, and a channel + // between them. + alice := ht.NewNodeWithCoins("Alice", nil) + bob := ht.NewNode("Bob", nil) + + ht.EnsureConnected(alice, bob) + + // We'll subscribe to channel events for both nodes. + aliceSub := alice.RPC.SubscribeChannelEvents() + bobSub := bob.RPC.SubscribeChannelEvents() + + // We'll then open a channel between Alice and Bob. + chanPoint := ht.OpenChannel( + alice, bob, lntest.OpenChannelParams{ + Amt: 1000000, + PushAmt: 500000, + }, + ) + + // We'll wait for the channel to be active. We expect to receive one + // pending, one open, and one active notification. + ht.AssertChannelActive(alice, chanPoint) + ht.AssertChannelActive(bob, chanPoint) + + ht.AssertChannelEventType( + aliceSub, lnrpc.ChannelEventUpdate_PENDING_OPEN_CHANNEL, + ) + ht.AssertChannelEventType( + aliceSub, lnrpc.ChannelEventUpdate_OPEN_CHANNEL, + ) + ht.AssertChannelEventType( + aliceSub, lnrpc.ChannelEventUpdate_ACTIVE_CHANNEL, + ) + + ht.AssertChannelEventType( + bobSub, lnrpc.ChannelEventUpdate_PENDING_OPEN_CHANNEL, + ) + ht.AssertChannelEventType( + bobSub, lnrpc.ChannelEventUpdate_OPEN_CHANNEL, + ) + ht.AssertChannelEventType( + bobSub, lnrpc.ChannelEventUpdate_ACTIVE_CHANNEL, + ) + + // We'll now make a payment from Alice to Bob to trigger a channel + // update. + payReqs, _, _ := ht.CreatePayReqs(bob, btcutil.Amount(1000), 1) + ht.CompletePaymentRequests(alice, payReqs) + + // assertUpdates is a helper function to assert the number of commitment + // updates received by a node. + assertUpdates := func(sub rpc.ChannelEventsClient, numUpdates int) { + event := ht.AssertChannelEventType( + sub, lnrpc.ChannelEventUpdate_CHANNEL_UPDATE, + ) + require.IsType( + ht, &lnrpc.ChannelEventUpdate_UpdatedChannel{}, + event.Channel, + ) + channel := event.GetUpdatedChannel().Channel + require.EqualValues(ht, numUpdates, channel.NumUpdates) + } + + // expectNoMoreUpdates is a helper function to assert that no more + // channel updates are received by a node. + expectNoMoreUpdates := func(sub rpc.ChannelEventsClient) { + updates := make(chan struct{}) + go func() { + _, err := sub.Recv() + // Only signal if we successfully received an update. + // If Recv fails (e.g., context canceled during test + // cleanup), that's fine - it means no update arrived. + if err == nil { + close(updates) + } + }() + + select { + case <-updates: + ht.Fatalf("expected no more updates") + case <-time.After(defaultTimeout): + } + } + + // We expect to see two updates from each node's point of view. One for + // the addition of the HTLC, and a second for the settlement. + assertUpdates(aliceSub, 1) + assertUpdates(aliceSub, 2) + expectNoMoreUpdates(aliceSub) + + assertUpdates(bobSub, 1) + assertUpdates(bobSub, 2) + expectNoMoreUpdates(bobSub) +} diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 8c60a2726..e2fcdebdc 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -425,6 +425,20 @@ func (h *HarnessTest) assertChannelStatus(hn *node.HarnessNode, return channel } +// AssertChannelEventType consumes one event from a client and asserts the event +// type is matched. +func (h *HarnessTest) AssertChannelEventType(sub rpc.ChannelEventsClient, + updateType lnrpc.ChannelEventUpdate_UpdateType, +) *lnrpc.ChannelEventUpdate { + + update := h.ReceiveChannelEvent(sub) + + require.Equalf(h, updateType, update.Type, "wrong event type, "+ + "want %v got %v", updateType, update.Type) + + return update +} + // AssertOutputScriptClass checks that the specified transaction output has the // expected script class. func (h *HarnessTest) AssertOutputScriptClass(tx *btcutil.Tx,