< Summary - Combined Code Coverage

Information
Class: NLightning.Domain.Channels.Factories.ChannelFactory
Assembly: NLightning.Domain
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Domain/Channels/Factories/ChannelFactory.cs
Tag: 36_15743069263
Line coverage
0%
Covered lines: 0
Uncovered lines: 116
Coverable lines: 116
Total lines: 291
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 78
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
CreateChannelV1AsNonInitiatorAsync()0%506220%
PerformOptionalChecks(...)0%156120%
PerformMandatoryChecks(...)0%1980440%

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Domain/Channels/Factories/ChannelFactory.cs

#LineLine coverage
 1using NLightning.Domain.Bitcoin.Transactions.Constants;
 2using NLightning.Domain.Bitcoin.Transactions.Outputs;
 3using NLightning.Domain.Protocol.Models;
 4
 5namespace NLightning.Domain.Channels.Factories;
 6
 7using Bitcoin.Interfaces;
 8using Bitcoin.ValueObjects;
 9using Constants;
 10using Crypto.Hashes;
 11using Crypto.ValueObjects;
 12using Domain.Enums;
 13using Enums;
 14using Exceptions;
 15using Interfaces;
 16using Models;
 17using Money;
 18using Node.Options;
 19using Protocol.Messages;
 20using Protocol.Payloads;
 21using Protocol.Tlv;
 22using ValueObjects;
 23
 24public class ChannelFactory : IChannelFactory
 25{
 26    private readonly IFeeService _feeService;
 27    private readonly ILightningSigner _lightningSigner;
 28    private readonly NodeOptions _nodeOptions;
 29    private readonly ISha256 _sha256;
 30
 031    public ChannelFactory(IFeeService feeService, ILightningSigner lightningSigner, NodeOptions nodeOptions,
 032                          ISha256 sha256)
 33    {
 034        _feeService = feeService;
 035        _lightningSigner = lightningSigner;
 036        _nodeOptions = nodeOptions;
 037        _sha256 = sha256;
 038    }
 39
 40    public async Task<ChannelModel> CreateChannelV1AsNonInitiatorAsync(OpenChannel1Message message,
 41                                                                       FeatureOptions negotiatedFeatures,
 42                                                                       CompactPubKey remoteNodeId)
 43    {
 044        var payload = message.Payload;
 45
 46        // If dual fund is negotiated fail the channel
 047        if (negotiatedFeatures.DualFund == FeatureSupport.Compulsory)
 048            throw new ChannelErrorException("We can only accept dual fund channels");
 49
 50        // Check if the channel type was negotiated and the channel type is present
 051        if (message.ChannelTypeTlv is not null && negotiatedFeatures.ChannelType == FeatureSupport.Compulsory)
 052            throw new ChannelErrorException("Channel type was negotiated but not provided");
 53
 54        // Perform optional checks for the channel
 055        PerformOptionalChecks(payload);
 56
 57        // Perform mandatory checks for the channel
 058        var currentFee = await _feeService.GetFeeRatePerKwAsync();
 059        PerformMandatoryChecks(message.ChannelTypeTlv, currentFee, negotiatedFeatures, payload, out var minimumDepth);
 60
 61        // Check for the upfront shutdown script
 062        if (message.UpfrontShutdownScriptTlv is null
 063         && (negotiatedFeatures.UpfrontShutdownScript > FeatureSupport.No || message.ChannelTypeTlv is not null))
 064            throw new ChannelErrorException("Upfront shutdown script is required but not provided");
 65
 066        BitcoinScript? remoteUpfrontShutdownScript = null;
 067        if (message.UpfrontShutdownScriptTlv is not null && message.UpfrontShutdownScriptTlv.Value.Length > 0)
 068            remoteUpfrontShutdownScript = message.UpfrontShutdownScriptTlv.Value;
 69
 70        // Calculate the amounts
 071        var toLocalAmount = payload.PushAmount;
 072        var toRemoteAmount = payload.FundingAmount - payload.PushAmount;
 73
 74        // Generate local keys through the signer
 075        var localKeyIndex = _lightningSigner.CreateNewChannel(out var localBasepoints, out var firstPerCommitmentPoint);
 76
 77        // Create the local key set
 078        var localKeySet = new ChannelKeySetModel(localKeyIndex, localBasepoints.FundingPubKey,
 079                                                 localBasepoints.RevocationBasepoint, localBasepoints.PaymentBasepoint,
 080                                                 localBasepoints.DelayedPaymentBasepoint, localBasepoints.HtlcBasepoint,
 081                                                 firstPerCommitmentPoint);
 82
 83        // Create the remote key set from the message
 084        var remoteKeySet = ChannelKeySetModel.CreateForRemote(message.Payload.FundingPubKey,
 085                                                              message.Payload.RevocationBasepoint,
 086                                                              message.Payload.PaymentBasepoint,
 087                                                              message.Payload.DelayedPaymentBasepoint,
 088                                                              message.Payload.HtlcBasepoint,
 089                                                              message.Payload.FirstPerCommitmentPoint);
 90
 091        BitcoinScript? localUpfrontShutdownScript = null;
 92        // Generate our upfront shutdown script
 093        if (_nodeOptions.Features.UpfrontShutdownScript > FeatureSupport.No)
 94        {
 95            // Generate our upfront shutdown script
 96            // TODO: Generate a script from the local key set
 97            // localUpfrontShutdownScript = ;
 98        }
 99
 100        // Generate the channel configuration
 0101        var useScidAlias = FeatureSupport.No;
 0102        if (negotiatedFeatures.ScidAlias > FeatureSupport.No)
 103        {
 0104            if (message.ChannelTypeTlv?.Features.IsFeatureSet(Feature.OptionScidAlias, true) ?? false)
 0105                useScidAlias = FeatureSupport.Compulsory;
 106            else
 0107                useScidAlias = FeatureSupport.Optional;
 108        }
 109
 0110        var channelConfig = new ChannelConfig(payload.ChannelReserveAmount, payload.FeeRatePerKw,
 0111                                              payload.HtlcMinimumAmount, _nodeOptions.DustLimitAmount,
 0112                                              payload.MaxAcceptedHtlcs, payload.MaxHtlcValueInFlight, minimumDepth,
 0113                                              negotiatedFeatures.AnchorOutputs != FeatureSupport.No,
 0114                                              payload.DustLimitAmount, payload.ToSelfDelay, useScidAlias,
 0115                                              localUpfrontShutdownScript, remoteUpfrontShutdownScript);
 116
 117        // Generate the commitment numbers
 0118        var commitmentNumber = new CommitmentNumber(remoteKeySet.PaymentCompactBasepoint,
 0119                                                    localKeySet.PaymentCompactBasepoint, _sha256);
 120
 121        try
 122        {
 0123            var fundingOutput = new FundingOutputInfo(payload.FundingAmount, localKeySet.FundingCompactPubKey,
 0124                                                      remoteKeySet.FundingCompactPubKey);
 125
 126            // Create the channel
 0127            return new ChannelModel(channelConfig, payload.ChannelId, commitmentNumber, fundingOutput, false, null,
 0128                                    null, toLocalAmount, localKeySet, 1, 0, toRemoteAmount, remoteKeySet, 1,
 0129                                    remoteNodeId, 0, ChannelState.V1Opening, ChannelVersion.V1);
 130        }
 0131        catch (Exception e)
 132        {
 0133            throw new ChannelErrorException("Error creating commitment transaction", e);
 134        }
 0135    }
 136
 137    /// <summary>
 138    /// Conducts optional validation checks on channel parameters to ensure compliance with acceptable ranges
 139    /// and configurations beyond the mandatory requirements.
 140    /// </summary>
 141    /// <remarks>
 142    /// This method verifies that optional configuration parameters meet recommended safety and usability thresholds:
 143    /// - Validates that the funding amount meets the minimum channel size threshold.
 144    /// - Checks that the HTLC minimum amount is not excessively large relative to the node's configured minimum value.
 145    /// - Validates that the maximum HTLC value in flight is enough relative to the channel funds.
 146    /// - Ensures the channel reserve amount is not excessively high relative to the node's channel reserve configuratio
 147    /// - Verifies that the maximum number of accepted HTLCs meets a minimum threshold.
 148    /// - Confirms that the dust limit is not excessively large relative to the node's configured dust limit.
 149    /// </remarks>
 150    /// <param name="payload">The payload containing the channel's configuration parameters, including funding amount, H
 151    /// <exception cref="ChannelErrorException">
 152    /// Thrown when one of the optional checks fails, including missing channel type when required, insufficient funding
 153    /// excessively high or low HTLC value limits, or incompatible reserve and dust limits.
 154    /// </exception>
 155    private void PerformOptionalChecks(OpenChannel1Payload payload)
 156    {
 157        // Check if Funding Satoshis is too small
 0158        if (payload.FundingAmount < _nodeOptions.MinimumChannelSize)
 0159            throw new ChannelErrorException($"Funding amount is too small: {payload.FundingAmount}");
 160
 161        // Check if we consider htlc_minimum_msat too large. IE. 20% bigger than our htlc minimum amount
 0162        if (payload.HtlcMinimumAmount > _nodeOptions.HtlcMinimumAmount * 1.2M)
 0163            throw new ChannelErrorException($"Htlc minimum amount is too large: {payload.HtlcMinimumAmount}");
 164
 165        // Check if we consider max_htlc_value_in_flight_msat too small. IE. 20% smaller than our maximum htlc value
 0166        var maxHtlcValueInFlight =
 0167            LightningMoney.Satoshis(_nodeOptions.AllowUpToPercentageOfChannelFundsInFlight *
 0168                                    payload.FundingAmount.Satoshi / 100M);
 0169        if (payload.MaxHtlcValueInFlight < maxHtlcValueInFlight * 0.8M)
 0170            throw new ChannelErrorException($"Max htlc value in flight is too small: {payload.MaxHtlcValueInFlight}");
 171
 172        // Check if we consider channel_reserve_satoshis too large. IE. 20% bigger than our channel reserve
 0173        if (payload.ChannelReserveAmount > _nodeOptions.ChannelReserveAmount * 1.2M)
 0174            throw new ChannelErrorException($"Channel reserve amount is too large: {payload.ChannelReserveAmount}");
 175
 176        // Check if we consider max_accepted_htlcs too small. IE. 20% smaller than our max-accepted htlcs
 0177        if (payload.MaxAcceptedHtlcs < (ushort)(_nodeOptions.MaxAcceptedHtlcs * 0.8M))
 0178            throw new ChannelErrorException($"Max accepted htlcs is too small: {payload.MaxAcceptedHtlcs}");
 179
 180        // Check if we consider dust_limit_satoshis too large. IE. 75% bigger than our dust limit
 0181        if (payload.DustLimitAmount > _nodeOptions.DustLimitAmount * 1.75M)
 0182            throw new ChannelErrorException($"Dust limit amount is too large: {payload.DustLimitAmount}");
 0183    }
 184
 185    /// <summary>
 186    /// Enforce mandatory checks when establishing a new Lightning Network channel.
 187    /// </summary>
 188    /// <remarks>
 189    /// The method validates channel parameters to ensure they comply with predefined safety and compatibility checks:
 190    /// - ChainHash must be compatible with the node's network.
 191    /// - Push amount must not exceed 1000 times the funding amount.
 192    /// - To_self_delay must not be unreasonably large compared to the node's configured value.
 193    /// - Max_accepted_htlcs must not exceed the allowed maximum.
 194    /// - Fee rate per kw must fall within acceptable limits.
 195    /// - Dust limit must be lower than or equal to the channel reserve amount and adhere to minimum thresholds.
 196    /// - Funding amount must be sufficient to cover fees and the channel reserve.
 197    /// - Large channels must only be supported if negotiated features include support for them.
 198    /// - Additional validation may apply to channel types based on negotiated options.
 199    /// </remarks>
 200    /// <param name="channelTypeTlv">Optional TLV data specifying the channel type, which may impose additional constrai
 201    /// <param name="currentFeeRatePerKw">The current network fee rate per kiloweight, used for fee validation.</param>
 202    /// <param name="negotiatedFeatures">Negotiated feature options between the participating nodes, affecting channel s
 203    /// <param name="payload">The payload containing the channel's configuration parameters and constraints.</param>
 204    /// <param name="minimumDepth">The minimum number of confirmations required for the channel to be considered operati
 205    /// <exception cref="ChannelErrorException">
 206    /// Thrown when any of the mandatory checks fail, such as invalid chain hash, excessive push amount, unreasonably la
 207    /// invalid funding amount, unsupported large channel, or mismatched channel type.
 208    /// </exception>
 209    private void PerformMandatoryChecks(ChannelTypeTlv? channelTypeTlv, LightningMoney currentFeeRatePerKw,
 210                                        FeatureOptions negotiatedFeatures, OpenChannel1Payload payload,
 211                                        out uint minimumDepth)
 212    {
 213        // Check if ChainHash is compatible
 0214        if (payload.ChainHash != _nodeOptions.BitcoinNetwork.ChainHash)
 0215            throw new ChannelErrorException("ChainHash is not compatible");
 216
 217        // Check if the push amount is too large
 0218        if (payload.PushAmount > 1_000 * payload.FundingAmount)
 0219            throw new ChannelErrorException($"Push amount is too large: {payload.PushAmount}");
 220
 221        // Check if we consider to_self_delay unreasonably large. IE. 50% bigger than our to_self_delay
 0222        if (payload.ToSelfDelay > _nodeOptions.ToSelfDelay * 1.5M)
 0223            throw new ChannelErrorException($"To self delay is too large: {payload.ToSelfDelay}");
 224
 225        // Check max_accepted_htlcs is too large
 0226        if (payload.MaxAcceptedHtlcs > ChannelConstants.MaxAcceptedHtlcs)
 0227            throw new ChannelErrorException($"Max accepted htlcs is too small: {payload.MaxAcceptedHtlcs}");
 228
 229        // Check if we consider fee_rate_per_kw too large
 0230        if (payload.FeeRatePerKw > ChannelConstants.MaxFeePerKw)
 0231            throw new ChannelErrorException($"Fee rate per kw is too large: {payload.FeeRatePerKw}");
 232
 233        // Check if we consider fee_rate_per_kw too small. IE. 20% smaller than our fee rate
 0234        if (payload.FeeRatePerKw < ChannelConstants.MinFeePerKw || payload.FeeRatePerKw < currentFeeRatePerKw * 0.8M)
 0235            throw new ChannelErrorException(
 0236                $"Fee rate per kw is too small: {payload.FeeRatePerKw}, currentFee{currentFeeRatePerKw}");
 237
 238        // Check if the dust limit is greater than the channel reserve amount
 0239        if (payload.DustLimitAmount > payload.ChannelReserveAmount)
 0240            throw new ChannelErrorException(
 0241                $"Dust limit({payload.DustLimitAmount}) is greater than channel reserve({payload.ChannelReserveAmount})"
 242
 243        // Check if dust_limit_satoshis is too small
 0244        if (payload.DustLimitAmount < ChannelConstants.MinDustLimitAmount)
 0245            throw new ChannelErrorException($"Dust limit amount is too small: {payload.DustLimitAmount}");
 246
 247        // Check if there are enough funds to pay for fees
 0248        var expectedWeight = negotiatedFeatures.AnchorOutputs > FeatureSupport.No
 0249                                 ? TransactionConstants.InitialCommitmentTransactionWeightNoAnchor
 0250                                 : TransactionConstants.InitialCommitmentTransactionWeightWithAnchor;
 0251        var expectedFee = LightningMoney.Satoshis(expectedWeight * currentFeeRatePerKw.Satoshi / 1000);
 0252        if (payload.FundingAmount < expectedFee + payload.ChannelReserveAmount)
 0253            throw new ChannelErrorException($"Funding amount is too small to cover fees: {payload.FundingAmount}");
 254
 255        // Check if this is a large channel and if we support it
 0256        if (payload.FundingAmount >= ChannelConstants.LargeChannelAmount &&
 0257            negotiatedFeatures.LargeChannels == FeatureSupport.No)
 0258            throw new ChannelErrorException("We don't support large channels");
 259
 260        // Check ChannelType against negotiated options
 0261        minimumDepth = _nodeOptions.MinimumDepth;
 0262        if (channelTypeTlv is not null)
 263        {
 264            // Check if it set any non-negotiated features
 0265            if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionStaticRemoteKey, true))
 266            {
 0267                if (negotiatedFeatures.StaticRemoteKey == FeatureSupport.No)
 0268                    throw new ChannelErrorException("Static remote key feature is not supported but requested by peer");
 269
 0270                if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionAnchorOutputs, true)
 0271                 && negotiatedFeatures.AnchorOutputs == FeatureSupport.No)
 0272                    throw new ChannelErrorException("Anchor outputs feature is not supported but requested by peer");
 273
 0274                if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionScidAlias, true))
 275                {
 0276                    if (payload.ChannelFlags.AnnounceChannel)
 0277                        throw new ChannelErrorException("Invalid channel flags for OPTION_SCID_ALIAS");
 278                }
 279
 280                // Check for ZeroConf feature
 0281                if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionZeroconf, true))
 282                {
 0283                    if (_nodeOptions.Features.ZeroConf == FeatureSupport.No)
 0284                        throw new ChannelErrorException("ZeroConf feature not supported but requested by peer");
 285
 0286                    minimumDepth = 0U;
 287                }
 288            }
 289        }
 0290    }
 291}