| | 1 | | using NLightning.Domain.Bitcoin.Transactions.Constants; |
| | 2 | | using NLightning.Domain.Bitcoin.Transactions.Outputs; |
| | 3 | | using NLightning.Domain.Protocol.Models; |
| | 4 | |
|
| | 5 | | namespace NLightning.Domain.Channels.Factories; |
| | 6 | |
|
| | 7 | | using Bitcoin.Interfaces; |
| | 8 | | using Bitcoin.ValueObjects; |
| | 9 | | using Constants; |
| | 10 | | using Crypto.Hashes; |
| | 11 | | using Crypto.ValueObjects; |
| | 12 | | using Domain.Enums; |
| | 13 | | using Enums; |
| | 14 | | using Exceptions; |
| | 15 | | using Interfaces; |
| | 16 | | using Models; |
| | 17 | | using Money; |
| | 18 | | using Node.Options; |
| | 19 | | using Protocol.Messages; |
| | 20 | | using Protocol.Payloads; |
| | 21 | | using Protocol.Tlv; |
| | 22 | | using ValueObjects; |
| | 23 | |
|
| | 24 | | public 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 | |
|
| 0 | 31 | | public ChannelFactory(IFeeService feeService, ILightningSigner lightningSigner, NodeOptions nodeOptions, |
| 0 | 32 | | ISha256 sha256) |
| | 33 | | { |
| 0 | 34 | | _feeService = feeService; |
| 0 | 35 | | _lightningSigner = lightningSigner; |
| 0 | 36 | | _nodeOptions = nodeOptions; |
| 0 | 37 | | _sha256 = sha256; |
| 0 | 38 | | } |
| | 39 | |
|
| | 40 | | public async Task<ChannelModel> CreateChannelV1AsNonInitiatorAsync(OpenChannel1Message message, |
| | 41 | | FeatureOptions negotiatedFeatures, |
| | 42 | | CompactPubKey remoteNodeId) |
| | 43 | | { |
| 0 | 44 | | var payload = message.Payload; |
| | 45 | |
|
| | 46 | | // If dual fund is negotiated fail the channel |
| 0 | 47 | | if (negotiatedFeatures.DualFund == FeatureSupport.Compulsory) |
| 0 | 48 | | 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 |
| 0 | 51 | | if (message.ChannelTypeTlv is not null && negotiatedFeatures.ChannelType == FeatureSupport.Compulsory) |
| 0 | 52 | | throw new ChannelErrorException("Channel type was negotiated but not provided"); |
| | 53 | |
|
| | 54 | | // Perform optional checks for the channel |
| 0 | 55 | | PerformOptionalChecks(payload); |
| | 56 | |
|
| | 57 | | // Perform mandatory checks for the channel |
| 0 | 58 | | var currentFee = await _feeService.GetFeeRatePerKwAsync(); |
| 0 | 59 | | PerformMandatoryChecks(message.ChannelTypeTlv, currentFee, negotiatedFeatures, payload, out var minimumDepth); |
| | 60 | |
|
| | 61 | | // Check for the upfront shutdown script |
| 0 | 62 | | if (message.UpfrontShutdownScriptTlv is null |
| 0 | 63 | | && (negotiatedFeatures.UpfrontShutdownScript > FeatureSupport.No || message.ChannelTypeTlv is not null)) |
| 0 | 64 | | throw new ChannelErrorException("Upfront shutdown script is required but not provided"); |
| | 65 | |
|
| 0 | 66 | | BitcoinScript? remoteUpfrontShutdownScript = null; |
| 0 | 67 | | if (message.UpfrontShutdownScriptTlv is not null && message.UpfrontShutdownScriptTlv.Value.Length > 0) |
| 0 | 68 | | remoteUpfrontShutdownScript = message.UpfrontShutdownScriptTlv.Value; |
| | 69 | |
|
| | 70 | | // Calculate the amounts |
| 0 | 71 | | var toLocalAmount = payload.PushAmount; |
| 0 | 72 | | var toRemoteAmount = payload.FundingAmount - payload.PushAmount; |
| | 73 | |
|
| | 74 | | // Generate local keys through the signer |
| 0 | 75 | | var localKeyIndex = _lightningSigner.CreateNewChannel(out var localBasepoints, out var firstPerCommitmentPoint); |
| | 76 | |
|
| | 77 | | // Create the local key set |
| 0 | 78 | | var localKeySet = new ChannelKeySetModel(localKeyIndex, localBasepoints.FundingPubKey, |
| 0 | 79 | | localBasepoints.RevocationBasepoint, localBasepoints.PaymentBasepoint, |
| 0 | 80 | | localBasepoints.DelayedPaymentBasepoint, localBasepoints.HtlcBasepoint, |
| 0 | 81 | | firstPerCommitmentPoint); |
| | 82 | |
|
| | 83 | | // Create the remote key set from the message |
| 0 | 84 | | var remoteKeySet = ChannelKeySetModel.CreateForRemote(message.Payload.FundingPubKey, |
| 0 | 85 | | message.Payload.RevocationBasepoint, |
| 0 | 86 | | message.Payload.PaymentBasepoint, |
| 0 | 87 | | message.Payload.DelayedPaymentBasepoint, |
| 0 | 88 | | message.Payload.HtlcBasepoint, |
| 0 | 89 | | message.Payload.FirstPerCommitmentPoint); |
| | 90 | |
|
| 0 | 91 | | BitcoinScript? localUpfrontShutdownScript = null; |
| | 92 | | // Generate our upfront shutdown script |
| 0 | 93 | | 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 |
| 0 | 101 | | var useScidAlias = FeatureSupport.No; |
| 0 | 102 | | if (negotiatedFeatures.ScidAlias > FeatureSupport.No) |
| | 103 | | { |
| 0 | 104 | | if (message.ChannelTypeTlv?.Features.IsFeatureSet(Feature.OptionScidAlias, true) ?? false) |
| 0 | 105 | | useScidAlias = FeatureSupport.Compulsory; |
| | 106 | | else |
| 0 | 107 | | useScidAlias = FeatureSupport.Optional; |
| | 108 | | } |
| | 109 | |
|
| 0 | 110 | | var channelConfig = new ChannelConfig(payload.ChannelReserveAmount, payload.FeeRatePerKw, |
| 0 | 111 | | payload.HtlcMinimumAmount, _nodeOptions.DustLimitAmount, |
| 0 | 112 | | payload.MaxAcceptedHtlcs, payload.MaxHtlcValueInFlight, minimumDepth, |
| 0 | 113 | | negotiatedFeatures.AnchorOutputs != FeatureSupport.No, |
| 0 | 114 | | payload.DustLimitAmount, payload.ToSelfDelay, useScidAlias, |
| 0 | 115 | | localUpfrontShutdownScript, remoteUpfrontShutdownScript); |
| | 116 | |
|
| | 117 | | // Generate the commitment numbers |
| 0 | 118 | | var commitmentNumber = new CommitmentNumber(remoteKeySet.PaymentCompactBasepoint, |
| 0 | 119 | | localKeySet.PaymentCompactBasepoint, _sha256); |
| | 120 | |
|
| | 121 | | try |
| | 122 | | { |
| 0 | 123 | | var fundingOutput = new FundingOutputInfo(payload.FundingAmount, localKeySet.FundingCompactPubKey, |
| 0 | 124 | | remoteKeySet.FundingCompactPubKey); |
| | 125 | |
|
| | 126 | | // Create the channel |
| 0 | 127 | | return new ChannelModel(channelConfig, payload.ChannelId, commitmentNumber, fundingOutput, false, null, |
| 0 | 128 | | null, toLocalAmount, localKeySet, 1, 0, toRemoteAmount, remoteKeySet, 1, |
| 0 | 129 | | remoteNodeId, 0, ChannelState.V1Opening, ChannelVersion.V1); |
| | 130 | | } |
| 0 | 131 | | catch (Exception e) |
| | 132 | | { |
| 0 | 133 | | throw new ChannelErrorException("Error creating commitment transaction", e); |
| | 134 | | } |
| 0 | 135 | | } |
| | 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 |
| 0 | 158 | | if (payload.FundingAmount < _nodeOptions.MinimumChannelSize) |
| 0 | 159 | | 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 |
| 0 | 162 | | if (payload.HtlcMinimumAmount > _nodeOptions.HtlcMinimumAmount * 1.2M) |
| 0 | 163 | | 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 |
| 0 | 166 | | var maxHtlcValueInFlight = |
| 0 | 167 | | LightningMoney.Satoshis(_nodeOptions.AllowUpToPercentageOfChannelFundsInFlight * |
| 0 | 168 | | payload.FundingAmount.Satoshi / 100M); |
| 0 | 169 | | if (payload.MaxHtlcValueInFlight < maxHtlcValueInFlight * 0.8M) |
| 0 | 170 | | 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 |
| 0 | 173 | | if (payload.ChannelReserveAmount > _nodeOptions.ChannelReserveAmount * 1.2M) |
| 0 | 174 | | 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 |
| 0 | 177 | | if (payload.MaxAcceptedHtlcs < (ushort)(_nodeOptions.MaxAcceptedHtlcs * 0.8M)) |
| 0 | 178 | | 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 |
| 0 | 181 | | if (payload.DustLimitAmount > _nodeOptions.DustLimitAmount * 1.75M) |
| 0 | 182 | | throw new ChannelErrorException($"Dust limit amount is too large: {payload.DustLimitAmount}"); |
| 0 | 183 | | } |
| | 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 |
| 0 | 214 | | if (payload.ChainHash != _nodeOptions.BitcoinNetwork.ChainHash) |
| 0 | 215 | | throw new ChannelErrorException("ChainHash is not compatible"); |
| | 216 | |
|
| | 217 | | // Check if the push amount is too large |
| 0 | 218 | | if (payload.PushAmount > 1_000 * payload.FundingAmount) |
| 0 | 219 | | 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 |
| 0 | 222 | | if (payload.ToSelfDelay > _nodeOptions.ToSelfDelay * 1.5M) |
| 0 | 223 | | throw new ChannelErrorException($"To self delay is too large: {payload.ToSelfDelay}"); |
| | 224 | |
|
| | 225 | | // Check max_accepted_htlcs is too large |
| 0 | 226 | | if (payload.MaxAcceptedHtlcs > ChannelConstants.MaxAcceptedHtlcs) |
| 0 | 227 | | throw new ChannelErrorException($"Max accepted htlcs is too small: {payload.MaxAcceptedHtlcs}"); |
| | 228 | |
|
| | 229 | | // Check if we consider fee_rate_per_kw too large |
| 0 | 230 | | if (payload.FeeRatePerKw > ChannelConstants.MaxFeePerKw) |
| 0 | 231 | | 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 |
| 0 | 234 | | if (payload.FeeRatePerKw < ChannelConstants.MinFeePerKw || payload.FeeRatePerKw < currentFeeRatePerKw * 0.8M) |
| 0 | 235 | | throw new ChannelErrorException( |
| 0 | 236 | | $"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 |
| 0 | 239 | | if (payload.DustLimitAmount > payload.ChannelReserveAmount) |
| 0 | 240 | | throw new ChannelErrorException( |
| 0 | 241 | | $"Dust limit({payload.DustLimitAmount}) is greater than channel reserve({payload.ChannelReserveAmount})" |
| | 242 | |
|
| | 243 | | // Check if dust_limit_satoshis is too small |
| 0 | 244 | | if (payload.DustLimitAmount < ChannelConstants.MinDustLimitAmount) |
| 0 | 245 | | throw new ChannelErrorException($"Dust limit amount is too small: {payload.DustLimitAmount}"); |
| | 246 | |
|
| | 247 | | // Check if there are enough funds to pay for fees |
| 0 | 248 | | var expectedWeight = negotiatedFeatures.AnchorOutputs > FeatureSupport.No |
| 0 | 249 | | ? TransactionConstants.InitialCommitmentTransactionWeightNoAnchor |
| 0 | 250 | | : TransactionConstants.InitialCommitmentTransactionWeightWithAnchor; |
| 0 | 251 | | var expectedFee = LightningMoney.Satoshis(expectedWeight * currentFeeRatePerKw.Satoshi / 1000); |
| 0 | 252 | | if (payload.FundingAmount < expectedFee + payload.ChannelReserveAmount) |
| 0 | 253 | | 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 |
| 0 | 256 | | if (payload.FundingAmount >= ChannelConstants.LargeChannelAmount && |
| 0 | 257 | | negotiatedFeatures.LargeChannels == FeatureSupport.No) |
| 0 | 258 | | throw new ChannelErrorException("We don't support large channels"); |
| | 259 | |
|
| | 260 | | // Check ChannelType against negotiated options |
| 0 | 261 | | minimumDepth = _nodeOptions.MinimumDepth; |
| 0 | 262 | | if (channelTypeTlv is not null) |
| | 263 | | { |
| | 264 | | // Check if it set any non-negotiated features |
| 0 | 265 | | if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionStaticRemoteKey, true)) |
| | 266 | | { |
| 0 | 267 | | if (negotiatedFeatures.StaticRemoteKey == FeatureSupport.No) |
| 0 | 268 | | throw new ChannelErrorException("Static remote key feature is not supported but requested by peer"); |
| | 269 | |
|
| 0 | 270 | | if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionAnchorOutputs, true) |
| 0 | 271 | | && negotiatedFeatures.AnchorOutputs == FeatureSupport.No) |
| 0 | 272 | | throw new ChannelErrorException("Anchor outputs feature is not supported but requested by peer"); |
| | 273 | |
|
| 0 | 274 | | if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionScidAlias, true)) |
| | 275 | | { |
| 0 | 276 | | if (payload.ChannelFlags.AnnounceChannel) |
| 0 | 277 | | throw new ChannelErrorException("Invalid channel flags for OPTION_SCID_ALIAS"); |
| | 278 | | } |
| | 279 | |
|
| | 280 | | // Check for ZeroConf feature |
| 0 | 281 | | if (channelTypeTlv.Features.IsFeatureSet(Feature.OptionZeroconf, true)) |
| | 282 | | { |
| 0 | 283 | | if (_nodeOptions.Features.ZeroConf == FeatureSupport.No) |
| 0 | 284 | | throw new ChannelErrorException("ZeroConf feature not supported but requested by peer"); |
| | 285 | |
|
| 0 | 286 | | minimumDepth = 0U; |
| | 287 | | } |
| | 288 | | } |
| | 289 | | } |
| 0 | 290 | | } |
| | 291 | | } |