Skip to content

chanevents+faraday: add chan events store and start#237

Open
bitromortac wants to merge 8 commits intolightninglabs:faraday-forwarding-abilityfrom
bitromortac:2604-fwd-prep-8
Open

chanevents+faraday: add chan events store and start#237
bitromortac wants to merge 8 commits intolightninglabs:faraday-forwarding-abilityfrom
bitromortac:2604-fwd-prep-8

Conversation

@bitromortac
Copy link
Copy Markdown
Contributor

Adds the channel event monitor and collects channel update events.

@bitromortac bitromortac requested a review from ViktorT-11 April 2, 2026 09:15
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new subsystem for monitoring and recording Lightning Network channel events. It adds the necessary infrastructure to track channel lifecycle events, such as opening, closing, and status updates, and integrates this monitoring into the main application lifecycle. The changes include database configuration support for both SQLite and Postgres to ensure persistent storage of these events.

Highlights

  • Channel Event Monitoring: Introduced a new chanevents package that monitors LND channel events and persists them to a database.
  • Database Integration: Added support for configuring and initializing SQLite or Postgres backends to store channel event data.
  • Initial Sync: Implemented an initial synchronization process to capture the current state of channels and peers upon startup.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

import (
"github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/faraday/accounting"
"github.com/lightninglabs/faraday/chanevents"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove the root logger mention from the commit message

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a channel events monitor to track LND channel state changes and record them in a persistent database, supporting both SQLite and Postgres backends. The implementation includes a background monitor loop, initial state synchronization, and integration into the main Faraday service lifecycle. Review feedback identifies several critical areas for improvement: implementing a reconnection strategy for the monitor loop to ensure resilience against transient LND connection failures, addressing a potential race condition between initial synchronization and event subscription, and optimizing the startup process to prevent redundant database entries. Additionally, a missing default case in the database store initialization was noted as a potential source of nil pointer panics.

Comment on lines +114 to +120
case err, ok := <-errChan:
if !ok {
log.Info("Channel event error stream closed")
return
}
log.Errorf("error from channel event subscription: %v", err)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The monitor loop terminates permanently if the channel event subscription encounters an error or the error channel is closed. This will cause the monitor to stop working silently until the entire daemon is restarted. It is recommended to implement a reconnection strategy with exponential backoff to ensure the monitor remains resilient to transient connection issues with lnd.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be addressed in a later PR.

Comment on lines +90 to +97
if err := m.initialSync(ctx); err != nil {
log.Errorf("error during initial sync: %v", err)
// We'll continue anyway, maybe the subscription will work.
}

// Subscribe to channel events.
// TODO: Add reconnection logic.
eventChan, errChan, err := m.lnd.SubscribeChannelEvents(ctx)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a potential race condition because initialSync is performed before starting the subscription. Any channel events occurring during the sync process or in the interval before the subscription is established might be missed. To avoid this, the subscription should be started first (to begin buffering events), followed by the initial sync, and then the buffered events should be processed while filtering out any that were already captured during the sync.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would address this in a later PR.

Comment on lines +134 to +232
func (m *Monitor) initialSync(ctx context.Context) error {
log.Info("Performing initial sync of channel state")

closedChannels, err := m.lnd.ClosedChannels(ctx)
if err != nil {
return fmt.Errorf("error listing closed channels: %w", err)
}

for _, channel := range closedChannels {
// Channels that didn't confirm onchain will be present here,
// but don't have a channel ID. We skip those.
if channel.ChannelID == 0 {
log.Debugf("Skipping closed channel with no "+
"channel ID: %s", channel.ChannelPoint)

continue
}

err := m.addChannel(
ctx, channel.PubKeyBytes, channel.ChannelPoint,
channel.ChannelID,
)

if err != nil {
log.Errorf("error adding closed channel %s: %v",
channel.ChannelPoint, err)

continue
}

dbChan, err := m.store.GetChannel(ctx, channel.ChannelPoint)
if err != nil {
log.Errorf("error getting closed channel %s from db: %v",
channel.ChannelPoint, err)

continue
}

if err := m.store.AddChannelEvent(ctx, &ChannelEvent{
ChannelID: dbChan.ID,
EventType: EventTypeOffline,
}); err != nil {
log.Errorf("error adding offline event for closed "+
"channel %s: %v", channel.ChannelPoint, err)
}
}

channels, err := m.lnd.ListChannels(ctx, false, false)
if err != nil {
return fmt.Errorf("error listing channels: %w", err)
}

for _, channel := range channels {
// We make sure the channel exists in the store.
err := m.addChannel(
ctx, channel.PubKeyBytes, channel.ChannelPoint,
channel.ChannelID,
)
if err != nil {
log.Errorf("error adding channel %s: %v",
channel.ChannelPoint, err)

continue
}

dbChan, err := m.store.GetChannel(ctx, channel.ChannelPoint)
if err != nil {
log.Errorf("error getting channel %s from db: %v",
channel.ChannelPoint, err)

continue
}

eventType := EventTypeOffline
if channel.Active {
eventType = EventTypeOnline
}

if err := m.store.AddChannelEvent(ctx, &ChannelEvent{
ChannelID: dbChan.ID,
EventType: eventType,
}); err != nil {
log.Errorf("error adding event for channel %s: %v",
channel.ChannelPoint, err)
}

if err := m.store.AddChannelEvent(ctx, &ChannelEvent{
ChannelID: dbChan.ID,
EventType: EventTypeUpdate,
LocalBalance: fn.Some(channel.LocalBalance),
RemoteBalance: fn.Some(channel.RemoteBalance),
}); err != nil {
log.Errorf("error adding event for channel %s: %v",
channel.ChannelPoint, err)
}
}

return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The initialSync process iterates through all closed and open channels and records Online/Offline and Update events on every startup. This approach has several issues:

  1. Database Bloat: It inserts redundant events even if the channel state hasn't changed since the last run.
  2. Scalability: For nodes with a large number of channels, this results in many database queries and inserts on every startup.
  3. Shutdown Responsiveness: The loops do not check if the context has been cancelled (ctx.Done()), which could delay shutdown if there are many channels to process.

Consider only recording events if the state has changed from the last known state in the database, and ensure the loops are context-aware.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true as well, but I think we want to address this later. An action I took was to add a isSync row to signal that the event was recorded during startup, to distinguish real events from startup sync.

This was forgotten previously.
Introduce requireEqualEvent to reduce boilerplate in store tests,
comparing all user-set fields while ignoring the auto-assigned ID.
We want to know if an update came from an initial sync. This also helps
us to identify data gaps and one can be sure it was not due to an actual
event. Modify the migration as it's unreleased.
Also refactor the root logger.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants