< 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: 57_24045730253
Line coverage
0%
Covered lines: 0
Uncovered lines: 159
Coverable lines: 159
Total lines: 331
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 52
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.AcceptChannel:
 99                // Handle the accept channel message
 0100                var acceptChannel1Message = message as AcceptChannel1Message
 0101                                         ?? throw new ChannelErrorException(
 0102                                                "Error boxing message to AcceptChannel1Message",
 0103                                                "Sorry, we had an internal error");
 0104                return await GetChannelMessageHandler<AcceptChannel1Message>(scope)
 0105                          .HandleAsync(acceptChannel1Message, currentState, negotiatedFeatures, peerPubKey);
 106
 107            case MessageTypes.FundingCreated:
 108                // Handle the funding-created message
 0109                var fundingCreatedMessage = message as FundingCreatedMessage
 0110                                         ?? throw new ChannelErrorException(
 0111                                                "Error boxing message to FundingCreatedMessage",
 0112                                                "Sorry, we had an internal error");
 0113                return await GetChannelMessageHandler<FundingCreatedMessage>(scope)
 0114                          .HandleAsync(fundingCreatedMessage, currentState, negotiatedFeatures, peerPubKey);
 115
 116            case MessageTypes.ChannelReady:
 117                // Handle channel ready message
 0118                var channelReadyMessage = message as ChannelReadyMessage
 0119                                       ?? throw new ChannelErrorException("Error boxing message to ChannelReadyMessage",
 0120                                                                          "Sorry, we had an internal error");
 0121                return await GetChannelMessageHandler<ChannelReadyMessage>(scope)
 0122                          .HandleAsync(channelReadyMessage, currentState, negotiatedFeatures, peerPubKey);
 123
 124            case MessageTypes.FundingSigned:
 125                // Handle funding signed message
 0126                var fundingSignedMessage = message as FundingSignedMessage
 0127                                        ?? throw new ChannelErrorException(
 0128                                               "Error boxing message to FundingSignedMessage",
 0129                                               "Sorry, we had an internal error");
 0130                return await GetChannelMessageHandler<FundingSignedMessage>(scope)
 0131                          .HandleAsync(fundingSignedMessage, currentState, negotiatedFeatures, peerPubKey);
 132
 133            default:
 0134                throw new ChannelErrorException("Unknown message type", "Sorry, we had an internal error");
 135        }
 0136    }
 137
 138    private IChannelMessageHandler<T> GetChannelMessageHandler<T>(IServiceScope scope)
 139        where T : IChannelMessage
 140    {
 0141        var handler = scope.ServiceProvider.GetRequiredService<IChannelMessageHandler<T>>() ??
 0142                      throw new ChannelErrorException($"No handler found for message type {typeof(T).FullName}",
 0143                                                      "Sorry, we had an internal error");
 0144        return handler;
 145    }
 146
 147    /// <summary>
 148    /// Persists a channel to the database using a scoped Unit of Work
 149    /// </summary>
 150    private async Task PersistChannelAsync(ChannelModel channel)
 151    {
 0152        using var scope = _serviceProvider.CreateScope();
 0153        var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 154
 155        try
 156        {
 157            // Check if the channel already exists
 158
 0159            _ = await unitOfWork.ChannelDbRepository.GetByIdAsync(channel.ChannelId)
 0160             ?? throw new ChannelWarningException("Channel not found", channel.ChannelId);
 0161            await unitOfWork.ChannelDbRepository.UpdateAsync(channel);
 0162            await unitOfWork.SaveChangesAsync();
 163
 164            // Remove from dictionaries
 0165            _channelMemoryRepository.TryRemoveChannel(channel.ChannelId);
 166
 0167            _logger.LogDebug("Successfully persisted channel {ChannelId} to database", channel.ChannelId);
 0168        }
 0169        catch (Exception ex)
 170        {
 0171            _logger.LogError(ex, "Failed to persist channel {ChannelId} to database", channel.ChannelId);
 0172            throw;
 173        }
 0174    }
 175
 176    private void HandleNewBlockDetected(object? sender, NewBlockEventArgs args)
 177    {
 0178        ArgumentNullException.ThrowIfNull(args);
 179
 0180        var currentHeight = (int)args.Height;
 181
 182        // Deal with stale channels
 0183        ForgetStaleChannels(currentHeight);
 184
 185        // Deal with channels that are waiting for funding confirmation on start-up
 0186        ConfirmUnconfirmedChannels(currentHeight);
 0187    }
 188
 189    private void ForgetStaleChannels(int currentHeight)
 190    {
 0191        var heightLimit = currentHeight - ChannelConstants.MaxUnconfirmedChannelAge;
 0192        if (heightLimit < 0)
 193        {
 0194            _logger.LogDebug("Block height {BlockHeight} is too low to forget channels", currentHeight);
 0195            return;
 196        }
 197
 0198        var staleChannels = _channelMemoryRepository.FindChannels(c => c.FundingCreatedAtBlockHeight <= heightLimit);
 199
 0200        _logger.LogDebug(
 0201            "Forgetting stale channels created before block height {HeightLimit}, found {StaleChannelCount} channels",
 0202            heightLimit, staleChannels.Count);
 203
 0204        foreach (var staleChannel in staleChannels)
 205        {
 0206            _logger.LogInformation(
 0207                "Forgetting stale channel {ChannelId} with funding created at block height {BlockHeight}",
 0208                staleChannel.ChannelId, staleChannel.FundingCreatedAtBlockHeight);
 209
 210            // Set states
 0211            staleChannel.UpdateState(ChannelState.Stale);
 0212            _channelMemoryRepository.UpdateChannel(staleChannel);
 213
 214            // Persist on Db
 215            try
 216            {
 0217                PersistChannelAsync(staleChannel).ContinueWith(task =>
 0218                {
 0219                    _logger.LogError(task.Exception, "Error while marking channel {channelId} as stale.",
 0220                                     staleChannel.ChannelId);
 0221                }, TaskContinuationOptions.OnlyOnFaulted);
 0222            }
 0223            catch (Exception e)
 224            {
 0225                _logger.LogError(e, "Failed to persist stale channel {ChannelId} to database at height {currentHeight}",
 0226                                 staleChannel.ChannelId, currentHeight);
 0227            }
 228        }
 0229    }
 230
 231    private void ConfirmUnconfirmedChannels(int currentHeight)
 232    {
 0233        using var scope = _serviceProvider.CreateScope();
 0234        using var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 235
 236        // Try to fetch the channel from memory
 0237        var unconfirmedChannels =
 0238            _channelMemoryRepository.FindChannels(c => c.State is ChannelState.ReadyForThem or ChannelState.ReadyForUs);
 239
 0240        foreach (var unconfirmedChannel in unconfirmedChannels)
 241        {
 242            // If the channel was created before the current block height, we can consider it confirmed
 0243            if (unconfirmedChannel.FundingCreatedAtBlockHeight <= currentHeight)
 244            {
 0245                if (unconfirmedChannel.FundingOutput.TransactionId is null)
 246                {
 0247                    _logger.LogError("Channel {ChannelId} has no funding transaction Id, cannot confirm",
 0248                                     unconfirmedChannel.ChannelId);
 0249                    continue;
 250                }
 251
 0252                var watchedTransaction =
 0253                    uow.WatchedTransactionDbRepository.GetByTransactionIdAsync(
 0254                        unconfirmedChannel.FundingOutput.TransactionId.Value).GetAwaiter().GetResult();
 0255                if (watchedTransaction is null)
 256                {
 0257                    _logger.LogError("Watched transaction for channel {ChannelId} not found",
 0258                                     unconfirmedChannel.ChannelId);
 0259                    continue;
 260                }
 261
 262                // Create a TransactionConfirmedEventArgs and call the event handler
 0263                var args = new TransactionConfirmedEventArgs(watchedTransaction, (uint)currentHeight);
 0264                HandleFundingConfirmationAsync(this, args);
 265            }
 266        }
 0267    }
 268
 269    private void HandleFundingConfirmationAsync(object? sender, TransactionConfirmedEventArgs args)
 270    {
 0271        ArgumentNullException.ThrowIfNull(args);
 0272        if (args.WatchedTransaction.FirstSeenAtHeight is null)
 273        {
 0274            _logger.LogError(
 0275                "Received null {nameof_FirstSeenAtHeight} in {nameof_TransactionConfirmedEventArgs} for channel {Channel
 0276                nameof(args.WatchedTransaction.FirstSeenAtHeight), nameof(TransactionConfirmedEventArgs),
 0277                args.WatchedTransaction.ChannelId);
 0278            return;
 279        }
 280
 0281        if (args.WatchedTransaction.TransactionIndex is null)
 282        {
 0283            _logger.LogError(
 0284                "Received null {nameof_FirstSeenAtHeight} in {nameof_TransactionConfirmedEventArgs} for channel {Channel
 0285                nameof(args.WatchedTransaction.FirstSeenAtHeight), nameof(TransactionConfirmedEventArgs),
 0286                args.WatchedTransaction.ChannelId);
 0287            return;
 288        }
 289
 290        // Create a scope to handle the funding confirmation
 0291        var scope = _serviceProvider.CreateScope();
 292
 0293        var channelId = args.WatchedTransaction.ChannelId;
 294        // Check if the transaction is a funding transaction for any channel
 0295        if (!_channelMemoryRepository.TryGetChannel(channelId, out var channel))
 296        {
 297            // Channel isn't found in memory, check the database
 0298            var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
 0299            channel = uow.ChannelDbRepository.GetByIdAsync(channelId).GetAwaiter().GetResult();
 0300            if (channel is null)
 301            {
 0302                _logger.LogError("Funding confirmation for unknown channel {ChannelId}", channelId);
 0303                return;
 304            }
 305
 0306            _lightningSigner.RegisterChannel(channelId, channel.GetSigningInfo());
 0307            _channelMemoryRepository.AddChannel(channel);
 308        }
 309
 0310        var fundingConfirmedHandler = scope.ServiceProvider.GetRequiredService<FundingConfirmedMessageHandler>();
 311
 312        // If we get a response, raise the event with the message
 0313        fundingConfirmedHandler.OnMessageReady += (_, message) =>
 0314            OnResponseMessageReady?.Invoke(this, new ChannelResponseMessageEventArgs(channel.RemoteNodeId, message));
 315
 316        // Add confirmation information to the channel
 0317        channel.FundingCreatedAtBlockHeight = args.WatchedTransaction.FirstSeenAtHeight.Value;
 0318        channel.ShortChannelId = new ShortChannelId(args.WatchedTransaction.FirstSeenAtHeight.Value,
 0319                                                    args.WatchedTransaction.TransactionIndex.Value,
 0320                                                    channel.FundingOutput.Index!.Value);
 321
 0322        fundingConfirmedHandler.HandleAsync(channel).ContinueWith(task =>
 0323        {
 0324            if (task.IsFaulted)
 0325                _logger.LogError(task.Exception, "Error while handling funding confirmation for channel {channelId}",
 0326                                 channel.ChannelId);
 0327
 0328            scope.Dispose();
 0329        });
 0330    }
 331}