Merge pull request #10543 from bitromortac/2602-channel-update-events

rpc: add channel updates to SubscribeChannelEvents
This commit is contained in:
Elle authored and GitHub committed 2026-02-23 13:09:36 +02:00
commit dd65ba2b01
16 files changed
+3257 -2828

No files matched your search

+99
View File
@@ -746,6 +746,33 @@ func (c *ChannelCommitment) extractTlvData() commitTlvData {
return auxData
}
// copy returns a deep copy of the channel commitment.
func (c *ChannelCommitment) copy() ChannelCommitment {
c2 := *c
if c.CommitTx != nil {
c2.CommitTx = c.CommitTx.Copy()
}
if len(c.CommitSig) > 0 {
c2.CommitSig = make([]byte, len(c.CommitSig))
copy(c2.CommitSig, c.CommitSig)
}
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
blobCopy := make([]byte, len(blob))
copy(blobCopy, blob)
c2.CustomBlob = fn.Some(blobCopy)
})
if len(c.Htlcs) > 0 {
c2.Htlcs = make([]HTLC, len(c.Htlcs))
for i, h := range c.Htlcs {
c2.Htlcs[i] = h.Copy()
}
}
return c2
}
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
// the default usable state, or a state where it shouldn't be used.
type ChannelStatus uint64
@@ -4066,6 +4093,78 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot {
return snapshot
}
// Copy returns a deep copy of the channel state.
func (c *OpenChannel) Copy() *OpenChannel {
c.RLock()
defer c.RUnlock()
clone := &OpenChannel{
ChanType: c.ChanType,
ChainHash: c.ChainHash,
FundingOutpoint: c.FundingOutpoint,
ShortChannelID: c.ShortChannelID,
IsPending: c.IsPending,
IsInitiator: c.IsInitiator,
chanStatus: c.chanStatus,
FundingBroadcastHeight: c.FundingBroadcastHeight,
ConfirmationHeight: c.ConfirmationHeight,
NumConfsRequired: c.NumConfsRequired,
ChannelFlags: c.ChannelFlags,
IdentityPub: c.IdentityPub,
Capacity: c.Capacity,
TotalMSatSent: c.TotalMSatSent,
TotalMSatReceived: c.TotalMSatReceived,
InitialLocalBalance: c.InitialLocalBalance,
InitialRemoteBalance: c.InitialRemoteBalance,
LocalChanCfg: c.LocalChanCfg,
RemoteChanCfg: c.RemoteChanCfg,
LocalCommitment: c.LocalCommitment.copy(),
RemoteCommitment: c.RemoteCommitment.copy(),
RemoteCurrentRevocation: c.RemoteCurrentRevocation,
RemoteNextRevocation: c.RemoteNextRevocation,
RevocationProducer: c.RevocationProducer,
RevocationStore: c.RevocationStore,
Packager: c.Packager,
ThawHeight: c.ThawHeight,
LastWasRevoke: c.LastWasRevoke,
RevocationKeyLocator: c.RevocationKeyLocator,
confirmedScid: c.confirmedScid,
TapscriptRoot: c.TapscriptRoot,
}
if c.FundingTxn != nil {
clone.FundingTxn = c.FundingTxn.Copy()
}
if len(c.LocalShutdownScript) > 0 {
clone.LocalShutdownScript = make(
lnwire.DeliveryAddress,
len(c.LocalShutdownScript),
)
copy(clone.LocalShutdownScript, c.LocalShutdownScript)
}
if len(c.RemoteShutdownScript) > 0 {
clone.RemoteShutdownScript = make(
lnwire.DeliveryAddress,
len(c.RemoteShutdownScript),
)
copy(clone.RemoteShutdownScript, c.RemoteShutdownScript)
}
if len(c.Memo) > 0 {
clone.Memo = make([]byte, len(c.Memo))
copy(clone.Memo, c.Memo)
}
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
blobCopy := make([]byte, len(blob))
copy(blobCopy, blob)
clone.CustomBlob = fn.Some(blobCopy)
})
return clone
}
// LatestCommitments returns the two latest commitments for both the local and
// remote party. These commitments are read from disk to ensure that only the
// latest fully committed state is returned. The first commitment returned is
+17
View File
@@ -72,6 +72,12 @@ type ClosedChannelEvent struct {
CloseSummary *channeldb.ChannelCloseSummary
}
// ChannelUpdateEvent represents a new event where a channel's state is updated.
type ChannelUpdateEvent struct {
// Channel is the channel that has been updated.
Channel *channeldb.OpenChannel
}
// FullyResolvedChannelEvent represents a new event where a channel becomes
// fully resolved.
type FullyResolvedChannelEvent struct {
@@ -238,3 +244,14 @@ func (c *ChannelNotifier) NotifyInactiveChannelEvent(chanPoint wire.OutPoint) {
log.Warnf("Unable to send inactive channel update: %v", err)
}
}
// NotifyChannelUpdateEvent notifies subscribers that a channel's state has been
// updated.
func (c *ChannelNotifier) NotifyChannelUpdateEvent(
channel *channeldb.OpenChannel) {
event := ChannelUpdateEvent{Channel: channel}
if err := c.ntfnServer.SendUpdate(event); err != nil {
log.Warnf("Unable to send channel update: %v", err)
}
}
+43
View File
@@ -0,0 +1,43 @@
package channelnotifier
import (
"testing"
"time"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/stretchr/testify/require"
)
// TestChannelUpdateEvent tests that channel update events are properly
// notified to subscribers.
func TestChannelUpdateEvent(t *testing.T) {
// Initialize the notification server.
ntfnServer := New(nil)
require.NoError(t, ntfnServer.Start())
defer func() {
require.NoError(t, ntfnServer.Stop())
}()
// Subscribe to channel events.
sub, err := ntfnServer.SubscribeChannelEvents()
require.NoError(t, err)
defer sub.Cancel()
// Create a mock channel state.
channel := &channeldb.OpenChannel{}
// Notify the server of a channel update event.
ntfnServer.NotifyChannelUpdateEvent(channel)
// Consume the event.
select {
case event := <-sub.Updates():
updateEvent, ok := event.(ChannelUpdateEvent)
require.True(t, ok)
require.Equal(t, channel, updateEvent.Channel)
case <-time.After(time.Second):
t.Fatalf("expected to receive channel update event")
}
}
@@ -129,6 +129,10 @@
composite `synced_to_chain` field which also considers router and blockbeat
dispatcher states.
* SubscribeChannelEvents [now emits channel update
events](https://github.com/lightningnetwork/lnd/pull/10543) to be able to
subscribe to state changes.
## lncli Updates
## Breaking Changes
@@ -189,6 +193,7 @@
# Contributors (Alphabetical Order)
* bitromortac
* Boris Nagaev
* Elle Mouton
* Erick Cestari
+11
View File
@@ -256,6 +256,10 @@ type ChannelLinkConfig struct {
// ChannelNotifier when a channel link become inactive.
NotifyInactiveLinkEvent func(wire.OutPoint)
// NotifyChannelUpdate allows the link to tell the ChannelNotifier when
// a channel's state has been updated.
NotifyChannelUpdate func(*channeldb.OpenChannel)
// HtlcNotifier is an instance of a htlcNotifier which we will pipe htlc
// events through.
HtlcNotifier htlcNotifier
@@ -1813,6 +1817,13 @@ func (l *channelLink) handleUpstreamMsg(ctx context.Context,
case *lnwire.CommitSig:
err = l.processRemoteCommitSig(ctx, msg)
// At this point our local commitment state has been irrevocably
// committed to and our balances are updated. We notify our
// subscribers that the channel state has been updated.
if err == nil {
l.cfg.NotifyChannelUpdate(l.channel.ChannelState())
}
case *lnwire.RevokeAndAck:
err = l.processRemoteRevokeAndAck(ctx, msg)
+2
View File
@@ -2241,6 +2241,7 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyChannelUpdate: func(*channeldb.OpenChannel) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
@@ -4931,6 +4932,7 @@ func (h *persistentLinkHarness) restartLink(
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
NotifyChannelUpdate: func(*channeldb.OpenChannel) {},
HtlcNotifier: h.hSwitch.cfg.HtlcNotifier,
SyncStates: syncStates,
GetAliases: getAliases,
+1
View File
@@ -1175,6 +1175,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
NotifyInactiveLinkEvent: func(wire.OutPoint) {},
NotifyChannelUpdate: func(*channeldb.OpenChannel) {},
HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
ShouldFwdExpAccountability: func() bool { return true },
+4
View File
@@ -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,
+99
View File
@@ -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)
}
+2917 -2827
View File
File diff suppressed because it is too large. Load diff
+6
View File
@@ -3064,6 +3064,10 @@ message PendingChannelsResponse {
message ChannelEventSubscription {
}
message ChannelCommitUpdate {
Channel channel = 1;
}
message ChannelEventUpdate {
oneof channel {
Channel open_channel = 1;
@@ -3073,6 +3077,7 @@ message ChannelEventUpdate {
PendingUpdate pending_open_channel = 6;
ChannelPoint fully_resolved_channel = 7;
ChannelPoint channel_funding_timeout = 8;
ChannelCommitUpdate updated_channel = 9;
}
enum UpdateType {
@@ -3083,6 +3088,7 @@ message ChannelEventUpdate {
PENDING_OPEN_CHANNEL = 4;
FULLY_RESOLVED_CHANNEL = 5;
CHANNEL_FUNDING_TIMEOUT = 6;
CHANNEL_UPDATE = 7;
}
UpdateType type = 5;
+13 -1
View File
@@ -3125,7 +3125,8 @@
"INACTIVE_CHANNEL",
"PENDING_OPEN_CHANNEL",
"FULLY_RESOLVED_CHANNEL",
"CHANNEL_FUNDING_TIMEOUT"
"CHANNEL_FUNDING_TIMEOUT",
"CHANNEL_UPDATE"
],
"default": "OPEN_CHANNEL"
},
@@ -4478,6 +4479,14 @@
}
}
},
"lnrpcChannelCommitUpdate": {
"type": "object",
"properties": {
"channel": {
"$ref": "#/definitions/lnrpcChannel"
}
}
},
"lnrpcChannelConstraints": {
"type": "object",
"properties": {
@@ -4609,6 +4618,9 @@
"channel_funding_timeout": {
"$ref": "#/definitions/lnrpcChannelPoint"
},
"updated_channel": {
"$ref": "#/definitions/lnrpcChannelCommitUpdate"
},
"type": {
"$ref": "#/definitions/ChannelEventUpdateUpdateType"
}
+14
View File
@@ -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,
+7
View File
@@ -6576,6 +6576,13 @@ func (lc *LightningChannel) ChannelPoint() wire.OutPoint {
return lc.channelState.FundingOutpoint
}
// ChannelState returns a copy of the internal channeldb.OpenChannel state
// struct. Modifications to the returned struct will not be reflected within
// the LightningChannel.
func (lc *LightningChannel) ChannelState() *channeldb.OpenChannel {
return lc.channelState.Copy()
}
// ChannelID returns the ChannelID of this LightningChannel. This is the same
// ChannelID that is used in update messages for this channel.
func (lc *LightningChannel) ChannelID() lnwire.ChannelID {
+1
View File
@@ -1480,6 +1480,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
NotifyActiveChannel: p.cfg.ChannelNotifier.NotifyActiveChannelEvent,
NotifyInactiveChannel: p.cfg.ChannelNotifier.NotifyInactiveChannelEvent,
NotifyInactiveLinkEvent: p.cfg.ChannelNotifier.NotifyInactiveLinkEvent,
NotifyChannelUpdate: p.cfg.ChannelNotifier.NotifyChannelUpdateEvent,
HtlcNotifier: p.cfg.HtlcNotifier,
GetAliases: p.cfg.GetAliases,
PreviouslySentShutdown: shutdownMsg,
+18
View File
@@ -5488,6 +5488,24 @@ func (r *rpcServer) SubscribeChannelEvents(req *lnrpc.ChannelEventSubscription,
},
}
case channelnotifier.ChannelUpdateEvent:
channel, err := createRPCOpenChannel(
updateStream.Context(),
r, event.Channel, true, false,
)
if err != nil {
return err
}
update = &lnrpc.ChannelEventUpdate{
Type: lnrpc.ChannelEventUpdate_CHANNEL_UPDATE,
Channel: &lnrpc.ChannelEventUpdate_UpdatedChannel{
UpdatedChannel: &lnrpc.ChannelCommitUpdate{
Channel: channel,
},
},
}
case channelnotifier.InactiveChannelEvent:
update = &lnrpc.ChannelEventUpdate{
Type: lnrpc.ChannelEventUpdate_INACTIVE_CHANNEL,