< Summary - Combined Code Coverage

Information
Class: NLightning.Application.Channels.Managers.ChannelManager
Assembly: NLightning.Application
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Application/Channels/Managers/ChannelManager.cs
Tag: 36_15743069263
Line coverage
0%
Covered lines: 0
Uncovered lines: 147
Coverable lines: 147
Total lines: 312
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Application/Channels/Managers/ChannelManager.cs

#LineLine coverage
 1using Microsoft.Extensions.DependencyInjection;
 2using Microsoft.Extensions.Logging;
 3
 4namespace NLightning.Application.Channels.Managers;
 5
 6using Domain.Bitcoin.Events;
 7using Domain.Bitcoin.Interfaces;
 8using Domain.Channels.Constants;
 9using Domain.Channels.Enums;
 10using Domain.Channels.Events;
 11using Domain.Channels.Interfaces;
 12using Domain.Channels.Models;
 13using Domain.Channels.ValueObjects;
 14using Domain.Crypto.ValueObjects;
 15using Domain.Exceptions;
 16using Domain.Node.Options;
 17using Domain.Persistence.Interfaces;
 18using Domain.Protocol.Constants;
 19using Domain.Protocol.Interfaces;
 20using Domain.Protocol.Messages;
 21using Handlers;
 22using Handlers.Interfaces;
 23using Infrastructure.Bitcoin.Wallet.Interfaces;
 24
 25public class ChannelManager : IChannelManager
 26{
 27    private readonly IChannelMemoryRepository _channelMemoryRepository;
 28    private readonly ILogger<ChannelManager> _logger;
 29    private readonly ILightningSigner _lightningSigner;
 30    private readonly IServiceProvider _serviceProvider;
 31
 32    public event EventHandler<ChannelResponseMessageEventArgs>? OnResponseMessageReady;
 33
 034    public ChannelManager(IBlockchainMonitor blockchainMonitor, IChannelMemoryRepository channelMemoryRepository,
 035                          ILogger<ChannelManager> logger, ILightningSigner lightningSigner,
 036                          IServiceProvider serviceProvider)
 37    {
 038        _channelMemoryRepository = channelMemoryRepository;
 039        _serviceProvider = serviceProvider;
 040        _logger = logger;
 041        _lightningSigner = lightningSigner;
 42
 043        blockchainMonitor.OnNewBlockDetected += HandleNewBlockDetected;
 044        blockchainMonitor.OnTransactionConfirmed += HandleFundingConfirmationAsync;
 045    }
 46
 47    public Task RegisterExistingChannelAsync(ChannelModel channel)
 48    {
 049        ArgumentNullException.ThrowIfNull(channel);
 50
 51        // Add the channel to the memory repository
 052        _channelMemoryRepository.AddChannel(channel);
 53
 54        // Register the channel with the signer
 055        _lightningSigner.RegisterChannel(channel.ChannelId, channel.GetSigningInfo());
 56
 057        _logger.LogInformation("Loaded channel {channelId} from database", channel.ChannelId);
 58
 59        // If the channel is open and ready
 060        if (channel.State == ChannelState.Open)
 61        {
 62            // TODO: Check if the channel has already been reestablished or if we need to reestablish it
 63        }
 064        else if (channel.State is ChannelState.ReadyForThem or ChannelState.ReadyForUs)
 65        {
 066            _logger.LogInformation("Waiting for channel {ChannelId} to be ready", channel.ChannelId);
 67        }
 68        else
 69        {
 70            // TODO: Deal with channels that are Closing, Stale, or any other state
 071            _logger.LogWarning("We don't know how to deal with {channelState} for channel {ChannelId}",
 072                               Enum.GetName(channel.State), channel.ChannelId);
 73        }
 74
 075        return Task.CompletedTask;
 76    }
 77
 78    public async Task<IChannelMessage?> HandleChannelMessageAsync(IChannelMessage message,
 79                                                                  FeatureOptions negotiatedFeatures,
 80                                                                  CompactPubKey peerPubKey)
 81    {
 082        using var scope = _serviceProvider.CreateScope();
 83
 84        // Check if the channel exists on the state dictionary
 085        _channelMemoryRepository.TryGetChannelState(message.Payload.ChannelId, out var currentState);
 86
 87        // In this case we can only handle messages that are opening a channel
 088        switch (message.Type)
 89        {
 90            case MessageTypes.OpenChannel:
 91                // Handle opening channel message
 092                var openChannel1Message = message as OpenChannel1Message
 093                                       ?? throw new ChannelErrorException("Error boxing message to OpenChannel1Message",
 094                                                                          "Sorry, we had an internal error");
 095                return await GetChannelMessageHandler<OpenChannel1Message>(scope)
 096                          .HandleAsync(openChannel1Message, currentState, negotiatedFeatures, peerPubKey);
 97
 98            case MessageTypes.FundingCreated:
 99                // Handle the funding-created message
 0100                var fundingCreatedMessage = message as FundingCreatedMessage
 0101                                         ?? throw new ChannelErrorException(
 0102                                                "Error boxing message to FundingCreatedMessage",
 0103                                                "Sorry, we had an internal error");
 0104                return await GetChannelMessageHandler<FundingCreatedMessage>(scope)
 0105                          .HandleAsync(fundingCreatedMessage, currentState, negotiatedFeatures, peerPubKey);
 106
 107            case MessageTypes.ChannelReady:
 108                // Handle channel ready message
 0109                var channelReadyMessage = message as ChannelReadyMessage
 0110                                       ?? throw new ChannelErrorException("Error boxing message to ChannelReadyMessage",
 0111                                                                          "Sorry, we had an internal error");
 0112                return await GetChannelMessageHandler<ChannelReadyMessage>(scope)
 0113                          .HandleAsync(channelReadyMessage, currentState, negotiatedFeatures, peerPubKey);
 114            default:
 0115                throw new ChannelErrorException("Unknown message type", "Sorry, we had an internal error");
 116        }
 0117    }
 118
 119    private IChannelMessageHandler<T> GetChannelMessageHandler<T>(IServiceScope scope)
 120        where T : IChannelMessage
 121    {
 0122        var handler = scope.ServiceProvider.GetRequiredService<IChannelMessageHandler<T>>() ??
 0123                      throw new ChannelErrorException($"No handler found for message type {typeof(T).FullName}",
 0124                                                      "Sorry, we had an internal error");
 0125        return handler;
 126    }
 127
 128    /// <summary>
 129    /// Persists a channel to the database using a scoped Unit of Work
 130    /// </summary>
 131    private async Task PersistChannelAsync(ChannelModel channel)
 132    {
 0133        using var scope = _serviceProvider.CreateScope();
 0134        var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 135
 136        try
 137        {
 138            // Check if the channel already exists
 139
 0140            _ = await unitOfWork.ChannelDbRepository.GetByIdAsync(channel.ChannelId)
 0141             ?? throw new ChannelWarningException("Channel not found", channel.ChannelId);
 0142            await unitOfWork.ChannelDbRepository.UpdateAsync(channel);
 0143            await unitOfWork.SaveChangesAsync();
 144
 145            // Remove from dictionaries
 0146            _channelMemoryRepository.RemoveChannel(channel.ChannelId);
 147
 0148            _logger.LogDebug("Successfully persisted channel {ChannelId} to database", channel.ChannelId);
 0149        }
 0150        catch (Exception ex)
 151        {
 0152            _logger.LogError(ex, "Failed to persist channel {ChannelId} to database", channel.ChannelId);
 0153            throw;
 154        }
 0155    }
 156
 157    private void HandleNewBlockDetected(object? sender, NewBlockEventArgs args)
 158    {
 0159        ArgumentNullException.ThrowIfNull(args);
 160
 0161        var currentHeight = (int)args.Height;
 162
 163        // Deal with stale channels
 0164        ForgetStaleChannels(currentHeight);
 165
 166        // Deal with channels that are waiting for funding confirmation on start-up
 0167        ConfirmUnconfirmedChannels(currentHeight);
 0168    }
 169
 170    private void ForgetStaleChannels(int currentHeight)
 171    {
 0172        var heightLimit = currentHeight - ChannelConstants.MaxUnconfirmedChannelAge;
 0173        if (heightLimit < 0)
 174        {
 0175            _logger.LogDebug("Block height {BlockHeight} is too low to forget channels", currentHeight);
 0176            return;
 177        }
 178
 0179        var staleChannels = _channelMemoryRepository.FindChannels(c => c.FundingCreatedAtBlockHeight <= heightLimit);
 180
 0181        _logger.LogDebug(
 0182            "Forgetting stale channels created before block height {HeightLimit}, found {StaleChannelCount} channels",
 0183            heightLimit, staleChannels.Count);
 184
 0185        foreach (var staleChannel in staleChannels)
 186        {
 0187            _logger.LogInformation(
 0188                "Forgetting stale channel {ChannelId} with funding created at block height {BlockHeight}",
 0189                staleChannel.ChannelId, staleChannel.FundingCreatedAtBlockHeight);
 190
 191            // Set states
 0192            staleChannel.UpdateState(ChannelState.Stale);
 0193            _channelMemoryRepository.UpdateChannel(staleChannel);
 194
 195            // Persist on Db
 196            try
 197            {
 0198                PersistChannelAsync(staleChannel).ContinueWith(task =>
 0199                {
 0200                    _logger.LogError(task.Exception, "Error while marking channel {channelId} as stale.",
 0201                                     staleChannel.ChannelId);
 0202                }, TaskContinuationOptions.OnlyOnFaulted);
 0203            }
 0204            catch (Exception e)
 205            {
 0206                _logger.LogError(e, "Failed to persist stale channel {ChannelId} to database at height {currentHeight}",
 0207                                 staleChannel.ChannelId, currentHeight);
 0208            }
 209        }
 0210    }
 211
 212    private void ConfirmUnconfirmedChannels(int currentHeight)
 213    {
 0214        using var scope = _serviceProvider.CreateScope();
 0215        using var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 216
 217        // Try to fetch the channel from memory
 0218        var unconfirmedChannels =
 0219            _channelMemoryRepository.FindChannels(c => c.State is ChannelState.ReadyForThem or ChannelState.ReadyForUs);
 220
 0221        foreach (var unconfirmedChannel in unconfirmedChannels)
 222        {
 223            // If the channel was created before the current block height, we can consider it confirmed
 0224            if (unconfirmedChannel.FundingCreatedAtBlockHeight <= currentHeight)
 225            {
 0226                if (unconfirmedChannel.FundingOutput.TransactionId is null)
 227                {
 0228                    _logger.LogError("Channel {ChannelId} has no funding transaction Id, cannot confirm",
 0229                                     unconfirmedChannel.ChannelId);
 0230                    continue;
 231                }
 232
 0233                var watchedTransaction =
 0234                    uow.WatchedTransactionDbRepository.GetByTransactionIdAsync(
 0235                        unconfirmedChannel.FundingOutput.TransactionId.Value).GetAwaiter().GetResult();
 0236                if (watchedTransaction is null)
 237                {
 0238                    _logger.LogError("Watched transaction for channel {ChannelId} not found",
 0239                                     unconfirmedChannel.ChannelId);
 0240                    continue;
 241                }
 242
 243                // Create a TransactionConfirmedEventArgs and call the event handler
 0244                var args = new TransactionConfirmedEventArgs(watchedTransaction, (uint)currentHeight);
 0245                HandleFundingConfirmationAsync(this, args);
 246            }
 247        }
 0248    }
 249
 250    private void HandleFundingConfirmationAsync(object? sender, TransactionConfirmedEventArgs args)
 251    {
 0252        ArgumentNullException.ThrowIfNull(args);
 0253        if (args.WatchedTransaction.FirstSeenAtHeight is null)
 254        {
 0255            _logger.LogError(
 0256                "Received null {nameof_FirstSeenAtHeight} in {nameof_TransactionConfirmedEventArgs} for channel {Channel
 0257                nameof(args.WatchedTransaction.FirstSeenAtHeight), nameof(TransactionConfirmedEventArgs),
 0258                args.WatchedTransaction.ChannelId);
 0259            return;
 260        }
 261
 0262        if (args.WatchedTransaction.TransactionIndex is null)
 263        {
 0264            _logger.LogError(
 0265                "Received null {nameof_FirstSeenAtHeight} in {nameof_TransactionConfirmedEventArgs} for channel {Channel
 0266                nameof(args.WatchedTransaction.FirstSeenAtHeight), nameof(TransactionConfirmedEventArgs),
 0267                args.WatchedTransaction.ChannelId);
 0268            return;
 269        }
 270
 271        // Create a scope to handle the funding confirmation
 0272        var scope = _serviceProvider.CreateScope();
 273
 0274        var channelId = args.WatchedTransaction.ChannelId;
 275        // Check if the transaction is a funding transaction for any channel
 0276        if (!_channelMemoryRepository.TryGetChannel(channelId, out var channel))
 277        {
 278            // Channel not found in memory, check the database
 0279            var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 0280            channel = uow.ChannelDbRepository.GetByIdAsync(channelId).GetAwaiter().GetResult();
 0281            if (channel is null)
 282            {
 0283                _logger.LogError("Funding confirmation for unknown channel {ChannelId}", channelId);
 0284                return;
 285            }
 286
 0287            _lightningSigner.RegisterChannel(channelId, channel.GetSigningInfo());
 0288            _channelMemoryRepository.AddChannel(channel);
 289        }
 290
 0291        var fundingConfirmedHandler = scope.ServiceProvider.GetRequiredService<FundingConfirmedHandler>();
 292
 293        // If we get a response, raise the event with the message
 0294        fundingConfirmedHandler.OnMessageReady += (_, message) =>
 0295            OnResponseMessageReady?.Invoke(this, new ChannelResponseMessageEventArgs(channel.RemoteNodeId, message));
 296
 297        // Add confirmation information to the channel
 0298        channel.FundingCreatedAtBlockHeight = args.WatchedTransaction.FirstSeenAtHeight.Value;
 0299        channel.ShortChannelId = new ShortChannelId(args.WatchedTransaction.FirstSeenAtHeight.Value,
 0300                                                    args.WatchedTransaction.TransactionIndex.Value,
 0301                                                    channel.FundingOutput.Index!.Value);
 302
 0303        fundingConfirmedHandler.HandleAsync(channel).ContinueWith(task =>
 0304        {
 0305            if (task.IsFaulted)
 0306                _logger.LogError(task.Exception, "Error while handling funding confirmation for channel {channelId}",
 0307                                 channel.ChannelId);
 0308
 0309            scope.Dispose();
 0310        });
 0311    }
 312}